diff --git a/moli-renderer-v8/src/callback_invocation.rs b/moli-renderer-v8/src/callback_invocation.rs index 7142858882..8ef53ac6d9 100644 --- a/moli-renderer-v8/src/callback_invocation.rs +++ b/moli-renderer-v8/src/callback_invocation.rs @@ -261,6 +261,11 @@ impl CallbackInvoker { invocation.relevant_context, invocation.incumbent_context, |scope| { + let relevant_dispatch_scope = invocation + .relevant_identity + .map(WindowExecutionContextIdentity::dispatch_scope); + let previous_relevant_dispatch_scope = + relevant_dispatch_scope.map(|dispatch_scope| dispatch_scope.enter(scope)); let relevant_context = invocation.relevant_context; let previous_window_event = invocation @@ -329,6 +334,11 @@ impl CallbackInvoker { let _ = global.set(scope, v8str(scope, WINDOW_EVENT_SLOT).into(), previous); } } + if let (Some(dispatch_scope), Some(previous_dispatch_scope)) = + (relevant_dispatch_scope, previous_relevant_dispatch_scope) + { + dispatch_scope.restore(scope, previous_dispatch_scope); + } completed }, ) diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs index bdd8c1fed9..99f570d337 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs @@ -9,6 +9,7 @@ use crate::{ context_bootstrap::{EventHandlerType, apply_event_handler_return_value}, exception_reporting::CallbackExceptionLogLevel, host::report_event_callback_exception, + native_bridge::lightweight_popup_id_from_window, util::{context_host_ptr_from_global_bridge, serialize_v8_array}, }; @@ -174,12 +175,34 @@ pub(crate) fn invoke_simple_event_listener<'s>( current_event: v8::Local<'s, v8::Object>, ) -> Option> { let invocation = listener.invocation(callback_this, arguments, Some(current_event)); + // Lightweight popup Window shells alias the opener's concrete V8 realm. + // Retain the callback's exact registration-time Window only for popup + // `load`: this is where a top-realm WPT callback must keep scheduling work + // on the opener after it calls `popup.close()`. Other synthetic popup + // events deliberately execute in their target owner scope until those + // Window shells gain distinct V8 realms. + let captured_relevant_identity = if event_type == "load" + && v8::Local::::try_from(callback_this) + .ok() + .and_then(|target| lightweight_popup_id_from_window(scope, target)) + .is_some() + { + listener.relevant_identity().filter(|identity| { + !matches!( + identity.dispatch_scope(), + crate::native_bridge::OwnerDispatchScope::LightweightPopup(_) + ) + }) + } else { + None + }; invoke_simple_event_callback_with_invocation( scope, event_type, callback_name, callback_this, listener.relevant_context(), + captured_relevant_identity, invocation, ) } @@ -213,6 +236,7 @@ fn invoke_simple_event_callback<'s>( callback_name, callback_this, relevant_context, + None, invocation, ) } @@ -239,12 +263,15 @@ fn invoke_simple_event_callback_with_invocation<'s>( callback_name: &str, callback_target: v8::Local<'s, v8::Value>, relevant_context: v8::Local<'s, v8::Context>, + captured_relevant_identity: Option, mut invocation: CallbackInvocation<'s, '_>, ) -> Option> { let host_ptr = context_host_ptr_from_global_bridge(scope); - let relevant_identity = host_ptr.and_then(|host_ptr| { - unsafe { &*host_ptr } - .window_execution_context_identity_for_v8_context(scope, relevant_context) + let relevant_identity = captured_relevant_identity.or_else(|| { + host_ptr.and_then(|host_ptr| { + unsafe { &*host_ptr } + .window_execution_context_identity_for_v8_context(scope, relevant_context) + }) }); if let Some(host_ptr) = host_ptr { invocation = invocation.with_execution_context_currentness(host_ptr, relevant_identity); diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/listeners.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/listeners.rs index 7a91bceed9..0e21f0b600 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/listeners.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/listeners.rs @@ -1,8 +1,10 @@ use super::*; use crate::abort_signal_route::{ResolvedAbortSignal, event_listener_signal_from_options_value}; use crate::callback_invocation::CallbackInvocation; +use crate::native_bridge::WindowExecutionContextIdentity; use crate::util::{ - get_private_object, get_private_value, new_null_prototype_object, set_private_value, v8_string, + context_host_ptr_from_global_bridge, get_private_object, get_private_value, + new_null_prototype_object, set_private_value, v8_string, }; use crate::webidl; use moli_webapi_declare::WebApiObject; @@ -13,6 +15,8 @@ const SIMPLE_EVENT_TARGET_LISTENER_RELEVANT_CONTEXT_ANCHOR_SLOT: &str = "__moliSimpleEventTargetListenerRelevantContextAnchor"; const SIMPLE_EVENT_TARGET_LISTENER_INCUMBENT_CONTEXT_ANCHOR_SLOT: &str = "__moliSimpleEventTargetListenerIncumbentContextAnchor"; +const SIMPLE_EVENT_TARGET_LISTENER_RELEVANT_IDENTITY_SLOT: &str = + "__moliSimpleEventTargetListenerRelevantIdentity"; const SIMPLE_EVENT_TARGET_LISTENER_CALLABLE_SLOT: &str = "__moliSimpleEventTargetListenerCallable"; const SIMPLE_EVENT_TARGET_LISTENER_CAPTURE_SLOT: &str = "__moliSimpleEventTargetListenerCapture"; const SIMPLE_EVENT_TARGET_LISTENER_ONCE_SLOT: &str = "__moliSimpleEventTargetListenerOnce"; @@ -84,6 +88,7 @@ pub(crate) struct SimpleObjectEventListenerSnapshot<'s> { callback: v8::Local<'s, v8::Object>, relevant_context: v8::Local<'s, v8::Context>, incumbent_context: v8::Local<'s, v8::Context>, + relevant_identity: Option, is_callable: bool, pub(crate) capture: bool, pub(crate) once: bool, @@ -107,6 +112,7 @@ struct SimpleObjectResolvedEventListener<'s> { callback: v8::Local<'s, v8::Object>, relevant_context_anchor: v8::Local<'s, v8::Object>, incumbent_context_anchor: v8::Local<'s, v8::Object>, + relevant_identity: Option, is_callable: bool, } @@ -133,6 +139,10 @@ impl<'s> SimpleObjectEventListenerSnapshot<'s> { self.relevant_context } + pub(crate) fn relevant_identity(&self) -> Option { + self.relevant_identity + } + pub(crate) fn callable_function(&self) -> Option> { self.is_callable .then(|| unsafe { v8::Local::::cast_unchecked(self.callback) }) @@ -274,13 +284,14 @@ pub(crate) fn simple_object_event_target_register_webidl_listener<'s>( .expect("converted EventListener callback must remain an object"); let relevant_context = listener.relevant_context(scope); let incumbent_context = listener.incumbent_context(scope); - let (relevant_context_anchor, incumbent_context_anchor) = + let (relevant_context_anchor, incumbent_context_anchor, relevant_identity) = simple_callback_context_anchors_for_contexts(scope, relevant_context, incumbent_context); let listener = SimpleObjectResolvedEventListener { original: callback.into(), callback, relevant_context_anchor, incumbent_context_anchor, + relevant_identity, is_callable: listener.callable_at_conversion(), }; simple_object_event_target_register_resolved_listener( @@ -319,6 +330,7 @@ fn simple_object_event_target_register_resolved_listener<'s>( listener.callback, listener.relevant_context_anchor, listener.incumbent_context_anchor, + listener.relevant_identity, listener.is_callable, options.capture, options.once, @@ -622,7 +634,7 @@ pub(crate) fn simple_object_event_set_ordered_handler<'s>( else { return; }; - let (relevant_context_anchor, incumbent_context_anchor) = + let (relevant_context_anchor, incumbent_context_anchor, relevant_identity) = simple_callback_context_anchors(scope, callback); for index in 0..listeners.length() { let Some(candidate) = listeners.get_index(scope, index) else { @@ -650,6 +662,11 @@ pub(crate) fn simple_object_event_set_ordered_handler<'s>( SIMPLE_EVENT_TARGET_LISTENER_INCUMBENT_CONTEXT_ANCHOR_SLOT, incumbent_context_anchor.into(), ); + set_simple_object_event_listener_relevant_identity( + scope, + entry, + relevant_identity, + ); } ensure_simple_object_event_type_order(scope, target, slot_name, event_type); return; @@ -661,6 +678,7 @@ pub(crate) fn simple_object_event_set_ordered_handler<'s>( callback, relevant_context_anchor, incumbent_context_anchor, + relevant_identity, ); let _ = listeners.set_index(scope, listeners.length(), entry.into()); ensure_simple_object_event_type_order(scope, target, slot_name, event_type); @@ -862,13 +880,14 @@ fn simple_object_event_listener_parts<'s>( return None; } let callback = v8::Local::::try_from(value).ok()?; - let (relevant_context_anchor, incumbent_context_anchor) = + let (relevant_context_anchor, incumbent_context_anchor, relevant_identity) = simple_callback_context_anchors(scope, callback); Some(SimpleObjectResolvedEventListener { original: value, callback, relevant_context_anchor, incumbent_context_anchor, + relevant_identity, is_callable: callback.is_callable(), }) } @@ -876,7 +895,11 @@ fn simple_object_event_listener_parts<'s>( fn simple_callback_context_anchors<'s>( scope: &mut v8::PinScope<'s, '_>, callback: v8::Local<'s, v8::Object>, -) -> (v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>) { +) -> ( + v8::Local<'s, v8::Object>, + v8::Local<'s, v8::Object>, + Option, +) { let current_context = scope.get_current_context(); let relevant_context = callback .get_creation_context(scope) @@ -889,7 +912,15 @@ fn simple_callback_context_anchors_for_contexts<'s>( scope: &mut v8::PinScope<'s, '_>, relevant_context: v8::Local<'s, v8::Context>, incumbent_context: v8::Local<'s, v8::Context>, -) -> (v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>) { +) -> ( + v8::Local<'s, v8::Object>, + v8::Local<'s, v8::Object>, + Option, +) { + let relevant_identity = context_host_ptr_from_global_bridge(scope).and_then(|host_ptr| { + unsafe { &*host_ptr } + .window_execution_context_identity_for_v8_context(scope, relevant_context) + }); let relevant_context_anchor = { let scope = &mut v8::ContextScope::new(scope, relevant_context); v8::Object::new(scope) @@ -898,7 +929,11 @@ fn simple_callback_context_anchors_for_contexts<'s>( let scope = &mut v8::ContextScope::new(scope, incumbent_context); v8::Object::new(scope) }; - (relevant_context_anchor, incumbent_context_anchor) + ( + relevant_context_anchor, + incumbent_context_anchor, + relevant_identity, + ) } fn simple_object_event_listener_entry_object<'s>( @@ -907,12 +942,13 @@ fn simple_object_event_listener_entry_object<'s>( callback: v8::Local<'s, v8::Object>, relevant_context_anchor: v8::Local<'s, v8::Object>, incumbent_context_anchor: v8::Local<'s, v8::Object>, + relevant_identity: Option, is_callable: bool, capture: bool, once: bool, passive: bool, ) -> v8::Local<'s, v8::Object> { - SimpleObjectEventListenerEntryDeclaration::new( + let entry = SimpleObjectEventListenerEntryDeclaration::new( original, callback, relevant_context_anchor, @@ -923,7 +959,9 @@ fn simple_object_event_listener_entry_object<'s>( passive, ) .bind(scope) - .expect("SimpleObject event listener entry declaration should bind") + .expect("SimpleObject event listener entry declaration should bind"); + set_simple_object_event_listener_relevant_identity(scope, entry, relevant_identity); + entry } fn simple_object_event_handler_entry_object<'s>( @@ -932,9 +970,10 @@ fn simple_object_event_handler_entry_object<'s>( callback: v8::Local<'s, v8::Object>, relevant_context_anchor: v8::Local<'s, v8::Object>, incumbent_context_anchor: v8::Local<'s, v8::Object>, + relevant_identity: Option, ) -> v8::Local<'s, v8::Object> { let marker = format!("event-handler:{handler_slot_name}"); - SimpleObjectEventHandlerEntryDeclaration::new( + let entry = SimpleObjectEventHandlerEntryDeclaration::new( v8_string(scope, &marker), handler_slot_name, callback, @@ -943,7 +982,26 @@ fn simple_object_event_handler_entry_object<'s>( true, ) .bind(scope) - .expect("SimpleObject event handler entry declaration should bind") + .expect("SimpleObject event handler entry declaration should bind"); + set_simple_object_event_listener_relevant_identity(scope, entry, relevant_identity); + entry +} + +fn set_simple_object_event_listener_relevant_identity<'s>( + scope: &mut v8::PinScope<'s, '_>, + entry: v8::Local<'s, v8::Object>, + identity: Option, +) { + let value = identity + .and_then(|identity| v8_string(scope, &identity.serialize_for_internal_slot())) + .map(v8::Local::::from) + .unwrap_or_else(|| v8::undefined(scope).into()); + set_private_value( + scope, + entry, + SIMPLE_EVENT_TARGET_LISTENER_RELEVANT_IDENTITY_SLOT, + value, + ); } fn simple_object_event_listener_array_contains_original( @@ -1046,6 +1104,16 @@ fn simple_object_event_listener_snapshot_entry<'s>( entry, SIMPLE_EVENT_TARGET_LISTENER_INCUMBENT_CONTEXT_ANCHOR_SLOT, )?; + let relevant_identity = get_private_value( + scope, + entry, + SIMPLE_EVENT_TARGET_LISTENER_RELEVANT_IDENTITY_SLOT, + ) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .and_then(|serialized| { + WindowExecutionContextIdentity::deserialize_from_internal_slot(&serialized) + }); let is_callable = simple_object_private_bool_slot(scope, entry, SIMPLE_EVENT_TARGET_LISTENER_CALLABLE_SLOT) .unwrap_or(false); @@ -1063,6 +1131,7 @@ fn simple_object_event_listener_snapshot_entry<'s>( callback, relevant_context, incumbent_context, + relevant_identity, is_callable, capture, once, diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 6d344d39ff..b975a0ef72 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -718,6 +718,18 @@ impl JsContextHost { .popup_load_event() } + pub(crate) fn page_popup_close_sender( + &self, + ) -> crate::page_task_queue::RendererPagePopupCloseSender { + self.page_task_capabilities + .get() + .expect( + "a live Page Window must install its complete Page task capabilities before popup close admission", + ) + .dom_manipulation() + .popup_close() + } + pub(crate) fn page_file_entry_file_callback_sender( &self, ) -> crate::page_task_queue::RendererPageFileEntryFileCallbackSender { diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index dfde73af94..c349ddd284 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -8,12 +8,14 @@ use crate::{ SharedWebStorageStore, WINDOW_NAME_SLOT, apply_local_window_location_navigation, deep_clone_shared_web_storage_store, dispatch_simple_event_target_event, install_navigation_bootstrap_entry_for_holder, install_simple_event_target_methods, - install_storage_aliases_for_window, + install_simple_event_target_ordered_handlers, install_storage_aliases_for_window, install_window_location_history_navigation_runtime_state, new_shared_web_storage_store, - scoped_indexed_db_factory, sync_document_location_runtime_state_from_window, + scoped_indexed_db_factory, simple_object_event_set_ordered_handler, + sync_document_location_runtime_state_from_window, sync_window_location_history_navigation_runtime_surface, sync_window_location_runtime_state, web_storage_area_key_for_storage_key, }, + definitions::define_function_accessor_property, document_runtime::create_content_security_policy_violation_event, document_runtime::{DocumentPolicyContainer, DocumentSandboxPolicy, DomHandle}, host::HostTimerOwner, @@ -444,13 +446,18 @@ pub(super) struct LightweightPopupBrowsingContextRecord { location_url: Url, opener_sandbox_policy: Option, lifecycle: LightweightPopupLifecycle, + is_closing: bool, navigation_id: LightweightPopupNavigationId, } impl LightweightPopupBrowsingContextRecord { - fn is_open(&self) -> bool { + fn is_live(&self) -> bool { matches!(self.lifecycle, LightweightPopupLifecycle::Open(_)) } + + fn is_open(&self) -> bool { + self.is_live() && !self.is_closing + } } pub(crate) struct OpenedLightweightPopup<'scope> { @@ -610,15 +617,30 @@ impl JsContextHost { .map(|record| record.navigation_id) } - fn close_lightweight_popup_browsing_context( + fn begin_lightweight_popup_close(&mut self, popup_id: u64) -> bool { + let Some(record) = self.lightweight_popup_record_mut(popup_id) else { + return false; + }; + if !record.is_open() { + return false; + } + record.is_closing = true; + true + } + + fn take_lightweight_popup_close_transition( &mut self, popup_id: u64, ) -> Option { let record = self.lightweight_popup_record_mut(popup_id)?; + if !record.is_live() || !record.is_closing { + return None; + } let lifecycle = std::mem::replace(&mut record.lifecycle, LightweightPopupLifecycle::Closed); let LightweightPopupLifecycle::Open(open) = lifecycle else { return None; }; + record.is_closing = false; record.navigation_id = LightweightPopupNavigationId::new( record .navigation_id @@ -635,6 +657,34 @@ impl JsContextHost { }) } + /// Definitely close a lightweight top-level browsing context after its + /// queued DOM-manipulation close task reaches the head of the source. + pub(crate) fn definitely_close_lightweight_popup_browsing_context<'s>( + &mut self, + scope: &mut v8::PinScope<'s, '_>, + popup_id: u64, + ) -> bool { + let Some(transition) = self.take_lightweight_popup_close_transition(popup_id) else { + return false; + }; + let window = self.lightweight_popup_window(scope, popup_id); + self.unregister_service_worker_popup_client(popup_id); + self.cancel_lightweight_popup_document_loads(popup_id); + self.cancel_lightweight_popup_classic_script_loads(popup_id); + if let Some(window) = window { + clear_lightweight_popup_window_document_event_state(scope, window); + } + if let Some(document_handle) = transition.retired_document_handle { + self.retire_lightweight_popup_document_handle(popup_id, document_handle); + self.clear_custom_element_registry_associations_for_document(document_handle); + } + self.retire_lightweight_popup_document_owner(transition.retired_owner); + self.retire_lightweight_popup_local_window(popup_id, transition.retired_local_window_id); + self.lightweight_popup_window_names + .retain(|_, named_popup_id| *named_popup_id != popup_id); + true + } + fn set_lightweight_popup_same_document_url(&mut self, popup_id: u64, url: Url) -> bool { let Some(record) = self.lightweight_popup_record_mut(popup_id) else { return false; @@ -824,6 +874,7 @@ impl JsContextHost { LIGHTWEIGHT_POPUP_EVENT_LISTENERS_SLOT, false, ); + install_lightweight_popup_event_handler_accessors(scope, window); let _ = LightweightPopupWindowMethodsDeclaration::default().initialize(scope, window); let initial_document_owner = self.allocate_lightweight_popup_document_owner(popup_id); let initial_local_window_id = self.allocate_lightweight_popup_local_window_id(); @@ -896,6 +947,7 @@ impl JsContextHost { }, session_storage_store, })), + is_closing: false, navigation_id: LightweightPopupNavigationId::new(1), }, ); @@ -1118,7 +1170,7 @@ impl JsContextHost { ) -> Option> { let (endpoint, opener) = { let record = self.lightweight_popup_browsing_contexts.get(&popup_id)?; - if !record.is_open() { + if !record.is_live() { return None; } let endpoint = record.opener?; @@ -1486,6 +1538,11 @@ impl JsContextHost { .is_some_and(LightweightPopupBrowsingContextRecord::is_open) } + pub(crate) fn lightweight_popup_is_closing(&self, popup_id: u64) -> bool { + self.lightweight_popup_record(popup_id) + .is_some_and(|record| record.is_live() && record.is_closing) + } + pub(crate) fn open_lightweight_popup_ids(&self) -> Vec { let mut popup_ids = self .lightweight_popup_browsing_contexts @@ -1758,7 +1815,7 @@ impl JsContextHost { document_handle: crate::document_runtime::DomHandle, ) -> bool { self.lightweight_popup_id_for_document_handle(document_handle) - .is_some() + .is_some_and(|popup_id| self.lightweight_popup_is_open(popup_id)) } pub(crate) fn current_lightweight_popup_document_owner( @@ -3637,7 +3694,7 @@ impl JsContextHost { document_remained_current = false; break; } - set_object_slot(scope, window, &name, value); + set_lightweight_popup_persisted_script_global(scope, window, &name, value); if !self.lightweight_popup_document_owner_is_current(script_document_owner) { document_remained_current = false; break; @@ -3664,7 +3721,7 @@ impl JsContextHost { if value.is_undefined() { continue; } - set_object_slot(scope, window, &name, value); + set_lightweight_popup_persisted_script_global(scope, window, &name, value); if !self.lightweight_popup_document_owner_is_current(script_document_owner) { document_remained_current = false; break; @@ -4394,27 +4451,21 @@ fn lightweight_popup_close_callback<'s>( return; }; let host = unsafe { &mut *host_ptr }; - let Some(transition) = host.close_lightweight_popup_browsing_context(popup_id) else { + if !host.begin_lightweight_popup_close(popup_id) { return; - }; + } set_object_slot( scope, window, "closed", v8::Boolean::new(scope, true).into(), ); - host.unregister_service_worker_popup_client(popup_id); - host.cancel_lightweight_popup_document_loads(popup_id); - host.cancel_lightweight_popup_classic_script_loads(popup_id); - clear_lightweight_popup_window_document_event_state(scope, window); - if let Some(document_handle) = transition.retired_document_handle { - host.retire_lightweight_popup_document_handle(popup_id, document_handle); - host.clear_custom_element_registry_associations_for_document(document_handle); + if host.page_popup_close_sender().send(popup_id).is_err() { + tracing::debug!( + popup_id, + "retired Page DOM-manipulation route rejected popup close task" + ); } - host.retire_lightweight_popup_document_owner(transition.retired_owner); - host.retire_lightweight_popup_local_window(popup_id, transition.retired_local_window_id); - host.lightweight_popup_window_names - .retain(|_, named_popup_id| *named_popup_id != popup_id); } fn lightweight_popup_initiator_endpoint<'s>( @@ -4752,6 +4803,89 @@ fn sync_lightweight_popup_window_location<'s>( } } +fn install_lightweight_popup_event_handler_accessors<'s>( + scope: &mut v8::PinScope<'s, '_>, + window: v8::Local<'s, v8::Object>, +) { + install_simple_event_target_ordered_handlers(scope, window); + for property_name in WINDOW_EVENT_HANDLER_PROPERTIES { + let data = v8str(scope, property_name).into(); + define_function_accessor_property( + scope, + window, + property_name, + lightweight_popup_event_handler_getter, + Some(data), + lightweight_popup_event_handler_setter, + Some(data), + v8::PropertyAttribute::NONE, + ) + .expect("lightweight popup Window event handler accessor should initialize"); + } +} + +fn lightweight_popup_event_handler_name<'s>( + scope: &mut v8::PinScope<'s, '_>, + data: v8::Local<'s, v8::Value>, +) -> Option<&'static str> { + let requested = data.to_string(scope)?.to_rust_string_lossy(scope); + WINDOW_EVENT_HANDLER_PROPERTIES + .iter() + .copied() + .find(|candidate| *candidate == requested) +} + +fn lightweight_popup_event_handler_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if lightweight_popup_id_from_window(scope, args.this()).is_none() { + rv.set_null(); + return; + } + let Some(property_name) = lightweight_popup_event_handler_name(scope, args.data()) else { + rv.set_null(); + return; + }; + rv.set( + get_private_value(scope, args.this(), property_name) + .filter(|value| value.is_function()) + .unwrap_or_else(|| v8::null(scope).into()), + ); +} + +fn lightweight_popup_event_handler_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if lightweight_popup_id_from_window(scope, args.this()).is_none() { + rv.set_undefined(); + return; + } + let Some(property_name) = lightweight_popup_event_handler_name(scope, args.data()) else { + rv.set_undefined(); + return; + }; + let value = args.get(0); + let stored = if value.is_function() { + value + } else { + v8::null(scope).into() + }; + set_private_value(scope, args.this(), property_name, stored); + simple_object_event_set_ordered_handler( + scope, + args.this(), + LIGHTWEIGHT_POPUP_EVENT_LISTENERS_SLOT, + property_name.strip_prefix("on").unwrap_or(property_name), + property_name, + stored.is_function(), + ); + rv.set_undefined(); +} + fn clear_lightweight_popup_window_document_event_state<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, @@ -4769,6 +4903,21 @@ fn clear_lightweight_popup_window_document_event_state<'s>( } } +fn set_lightweight_popup_persisted_script_global<'s>( + scope: &mut v8::PinScope<'s, '_>, + window: v8::Local<'s, v8::Object>, + name: &str, + value: v8::Local<'s, v8::Value>, +) { + if WINDOW_EVENT_HANDLER_PROPERTIES.contains(&name) { + // The wrapped popup script already assigned this value while its + // exact popup owner scope was active. Do not invoke the setter again + // from the surrounding top scope or replace its accessor. + return; + } + set_object_slot(scope, window, name, value); +} + fn lightweight_popup_effective_navigation_kind( current_url: Option<&Url>, kind: crate::context_bootstrap::LocationNavigationKind, diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/registry.rs b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/registry.rs index 1eb6ed342e..2968feba8c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/registry.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/registry.rs @@ -216,9 +216,157 @@ impl WindowExecutionContextIdentity { self.access_policy == WindowExecutionContextAccessPolicy::Universal } + /// Encodes an exact callback-realm identity for storage in an internal V8 + /// private slot. Lightweight popup realms alias their opener's V8 context, + /// so retaining only that context would lose the LocalWindow generation + /// and dispatch address captured when the callback was converted. + pub(crate) fn serialize_for_internal_slot(self) -> String { + let policy = u8::from(self.grants_universal_access()); + match (self.owner, self.dispatch_scope) { + (WindowExecutionContextOwner::Frame(local_window_id), OwnerDispatchScope::Top) => { + format!( + "frame:{}:top:{}:{policy}", + local_window_id.0, + self.realm_token.as_u64() + ) + } + ( + WindowExecutionContextOwner::Frame(local_window_id), + OwnerDispatchScope::Child(child_handle), + ) => format!( + "frame:{}:child:{}:{}:{policy}", + local_window_id.0, + child_handle.index(), + self.realm_token.as_u64() + ), + ( + WindowExecutionContextOwner::LightweightPopup { + popup_id, + local_window_id, + }, + OwnerDispatchScope::LightweightPopup(dispatch_popup_id), + ) => { + debug_assert_eq!(popup_id, dispatch_popup_id); + format!( + "popup:{popup_id}:{}:{}:{policy}", + local_window_id.as_u64(), + self.realm_token.as_u64() + ) + } + _ => unreachable!("Window execution-context owner and dispatch scope diverged"), + } + } + + pub(crate) fn deserialize_from_internal_slot(serialized: &str) -> Option { + fn access_policy(value: &str) -> Option { + match value { + "0" => Some(WindowExecutionContextAccessPolicy::EnforceWebOrigin), + "1" => Some(WindowExecutionContextAccessPolicy::Universal), + _ => None, + } + } + + let parts = serialized.split(':').collect::>(); + match parts.as_slice() { + ["frame", local_window_id, "top", realm_token, policy] => Some(Self::new( + WindowExecutionContextOwner::Frame(crate::frame_owner_model::LocalWindowId( + local_window_id.parse().ok()?, + )), + OwnerDispatchScope::Top, + RuntimeObservableContextToken::from_raw(realm_token.parse().ok()?), + access_policy(policy)?, + )), + [ + "frame", + local_window_id, + "child", + child_handle, + realm_token, + policy, + ] => { + let child_handle = child_handle.parse::().ok()?; + let child_handle = usize::try_from(child_handle).ok()?; + Some(Self::new( + WindowExecutionContextOwner::Frame(crate::frame_owner_model::LocalWindowId( + local_window_id.parse().ok()?, + )), + OwnerDispatchScope::Child(crate::document_runtime::DomHandle::new( + child_handle, + )), + RuntimeObservableContextToken::from_raw(realm_token.parse().ok()?), + access_policy(policy)?, + )) + } + ["popup", popup_id, local_window_id, realm_token, policy] => { + let popup_id = popup_id.parse().ok()?; + Some(Self::new( + WindowExecutionContextOwner::LightweightPopup { + popup_id, + local_window_id: LightweightPopupLocalWindowId::new( + local_window_id.parse().ok()?, + ), + }, + OwnerDispatchScope::LightweightPopup(popup_id), + RuntimeObservableContextToken::from_raw(realm_token.parse().ok()?), + access_policy(policy)?, + )) + } + _ => None, + } + } + pub(in crate::native_bridge::context_host) fn access_policy( self, ) -> WindowExecutionContextAccessPolicy { self.access_policy } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn callback_identity_internal_slot_encoding_round_trips_every_owner_shape() { + let identities = [ + WindowExecutionContextIdentity::new( + WindowExecutionContextOwner::Frame(crate::frame_owner_model::LocalWindowId(7)), + OwnerDispatchScope::Top, + RuntimeObservableContextToken::from_raw(11), + WindowExecutionContextAccessPolicy::EnforceWebOrigin, + ), + WindowExecutionContextIdentity::new( + WindowExecutionContextOwner::Frame(crate::frame_owner_model::LocalWindowId(13)), + OwnerDispatchScope::Child(crate::document_runtime::DomHandle::new(17)), + RuntimeObservableContextToken::from_raw(19), + WindowExecutionContextAccessPolicy::Universal, + ), + WindowExecutionContextIdentity::new( + WindowExecutionContextOwner::LightweightPopup { + popup_id: 23, + local_window_id: LightweightPopupLocalWindowId::new(29), + }, + OwnerDispatchScope::LightweightPopup(23), + RuntimeObservableContextToken::from_raw(31), + WindowExecutionContextAccessPolicy::EnforceWebOrigin, + ), + ]; + + for identity in identities { + assert_eq!( + WindowExecutionContextIdentity::deserialize_from_internal_slot( + &identity.serialize_for_internal_slot() + ), + Some(identity) + ); + } + assert_eq!( + WindowExecutionContextIdentity::deserialize_from_internal_slot("popup:23:29:31:2"), + None + ); + assert_eq!( + WindowExecutionContextIdentity::deserialize_from_internal_slot("not-an-identity"), + None + ); + } +} diff --git a/moli-renderer-v8/src/page_task_queue.rs b/moli-renderer-v8/src/page_task_queue.rs index 3abd772648..7e11c47146 100644 --- a/moli-renderer-v8/src/page_task_queue.rs +++ b/moli-renderer-v8/src/page_task_queue.rs @@ -34,6 +34,7 @@ mod owner_sources; mod parse_time; mod parser_async_module_admission; mod parser_owned_module_continuation; +mod popup_close; mod popup_load_event; mod post_domcontentloaded_runtime; mod post_parse_owner_work; @@ -371,6 +372,10 @@ pub(crate) use self::parser_owned_module_continuation::{ PageParserOwnedModuleContinuationBodyActivity, PageParserOwnedModuleContinuationTargetEffect, PageParserOwnedModuleContinuationTurnAction, }; +pub(crate) use self::popup_close::{ + PagePopupCloseTargetEffect, PagePopupCloseTurnAction, PagePopupCloseTurnOutcome, + RendererPagePopupCloseOwner, RendererPagePopupCloseSender, RendererPagePopupCloseTask, +}; pub(crate) use self::popup_load_event::{ PagePopupLoadEventTargetEffect, PagePopupLoadEventTurnAction, PagePopupLoadEventTurnOutcome, RendererPagePopupLoadEventOwner, RendererPagePopupLoadEventSender, diff --git a/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs b/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs index 7db979f0a1..72d3e834f7 100644 --- a/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs +++ b/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs @@ -27,6 +27,9 @@ use super::{ RendererPageImageLoadEventOwner, RendererPageImageLoadEventSender, RendererPageImageLoadEventTask, }, + popup_close::{ + RendererPagePopupCloseOwner, RendererPagePopupCloseSender, RendererPagePopupCloseTask, + }, popup_load_event::{ RendererPagePopupLoadEventOwner, RendererPagePopupLoadEventSender, RendererPagePopupLoadEventTask, @@ -66,6 +69,7 @@ pub(crate) enum RendererPageDomManipulationOwner { FileEntryFileCallback(RendererPageFileEntryFileCallbackOwner), ImageLoadEvent(RendererPageImageLoadEventOwner), PopupLoadEvent(RendererPagePopupLoadEventOwner), + PopupClose(RendererPagePopupCloseOwner), ConnectedStyleEvent(RendererPageStylesheetTaskOwner), TextTrackDefaultMode(RendererPageTextTrackDefaultModeOwner), TextTrackLoad(RendererPageTextTrackLoadOwner), @@ -82,6 +86,7 @@ pub(crate) enum RendererPageDomManipulationTask { FileEntryFileCallback(RendererPageFileEntryFileCallbackTask), ImageLoadEvent(RendererPageImageLoadEventTask), PopupLoadEvent(RendererPagePopupLoadEventTask), + PopupClose(RendererPagePopupCloseTask), ConnectedStyleEvent(RendererPageConnectedStyleEventTask), TextTrackDefaultMode(RendererPageTextTrackDefaultModeTask), TextTrackLoad(RendererPageTextTrackLoadTask), @@ -110,6 +115,7 @@ impl RendererPageDomManipulationTask { Self::PopupLoadEvent(task) => { RendererPageDomManipulationOwner::PopupLoadEvent(task.owner()) } + Self::PopupClose(task) => RendererPageDomManipulationOwner::PopupClose(task.owner()), Self::ConnectedStyleEvent(task) => { RendererPageDomManipulationOwner::ConnectedStyleEvent(task.owner()) } @@ -144,6 +150,7 @@ pub(crate) enum PageDomManipulationTurnAction { FileEntryFileCallback(super::PageFileEntryFileCallbackTurnAction), ImageLoadEvent(super::PageImageLoadEventTurnAction), PopupLoadEvent(super::PagePopupLoadEventTurnAction), + PopupClose(super::PagePopupCloseTurnAction), ConnectedStyleEvent(PageConnectedStyleEventTurnAction), TextTrackDefaultMode(super::PageTextTrackDefaultModeTurnAction), TextTrackLoad(super::PageTextTrackLoadTurnAction), @@ -203,6 +210,10 @@ impl RendererPageDomManipulationSender { RendererPagePopupLoadEventSender::new(self.route.clone(), self.root_document) } + pub(crate) fn popup_close(&self) -> RendererPagePopupCloseSender { + RendererPagePopupCloseSender::new(self.route.clone(), self.root_document) + } + pub(crate) fn text_track_default_mode(&self) -> RendererPageTextTrackDefaultModeSender { RendererPageTextTrackDefaultModeSender::new(self.route.clone(), self.root_document) } diff --git a/moli-renderer-v8/src/page_task_queue/popup_close.rs b/moli-renderer-v8/src/page_task_queue/popup_close.rs new file mode 100644 index 0000000000..6af9388705 --- /dev/null +++ b/moli-renderer-v8/src/page_task_queue/popup_close.rs @@ -0,0 +1,89 @@ +use crate::runtime::{PageOwnerTurnOutcome, RendererDocumentToken}; + +use super::dom_manipulation::{RendererPageDomManipulationRoute, RendererPageDomManipulationTask}; + +/// PageVm namespace plus the top-level lightweight browsing context whose +/// script-visible closing flag has been set. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct RendererPagePopupCloseOwner { + root_document: RendererDocumentToken, + popup_id: u64, +} + +impl RendererPagePopupCloseOwner { + pub(crate) const fn new(root_document: RendererDocumentToken, popup_id: u64) -> Self { + Self { + root_document, + popup_id, + } + } + + pub(crate) const fn root_document(self) -> RendererDocumentToken { + self.root_document + } + + pub(crate) const fn popup_id(self) -> u64 { + self.popup_id + } +} + +#[derive(Debug)] +pub(crate) struct RendererPagePopupCloseTask { + owner: RendererPagePopupCloseOwner, +} + +impl RendererPagePopupCloseTask { + fn new(owner: RendererPagePopupCloseOwner) -> Self { + Self { owner } + } + + pub(crate) const fn owner(&self) -> RendererPagePopupCloseOwner { + self.owner + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct RendererPagePopupCloseRouteClosed; + +#[derive(Clone, Debug)] +pub(crate) struct RendererPagePopupCloseSender { + route: RendererPageDomManipulationRoute, + root_document: RendererDocumentToken, +} + +impl RendererPagePopupCloseSender { + pub(super) fn new( + route: RendererPageDomManipulationRoute, + root_document: RendererDocumentToken, + ) -> Self { + Self { + route, + root_document, + } + } + + pub(crate) fn send(&self, popup_id: u64) -> Result<(), RendererPagePopupCloseRouteClosed> { + let owner = RendererPagePopupCloseOwner::new(self.root_document, popup_id); + self.route + .send(RendererPageDomManipulationTask::PopupClose( + RendererPagePopupCloseTask::new(owner), + )) + .map_err(|_| RendererPagePopupCloseRouteClosed) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum PagePopupCloseTargetEffect { + DefinitelyClosed, + DiscardedStaleOwner { + current_owner: Option, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct PagePopupCloseTurnAction { + pub(crate) owner: RendererPagePopupCloseOwner, + pub(crate) target_effect: PagePopupCloseTargetEffect, +} + +pub(crate) type PagePopupCloseTurnOutcome = PageOwnerTurnOutcome; diff --git a/moli-renderer-v8/src/runtime/mod.rs b/moli-renderer-v8/src/runtime/mod.rs index 5990beaade..74b2d923cd 100644 --- a/moli-renderer-v8/src/runtime/mod.rs +++ b/moli-renderer-v8/src/runtime/mod.rs @@ -111,6 +111,7 @@ pub(crate) use self::page_vm::AuthorizedCurrentPageMiscPlatformApiTask; pub(crate) use self::page_vm::AuthorizedCurrentPageModuleReaction; pub(crate) use self::page_vm::AuthorizedCurrentPageNavigationApiTask; pub(crate) use self::page_vm::AuthorizedCurrentPageOpfsTask; +pub(crate) use self::page_vm::AuthorizedCurrentPagePopupClose; pub(crate) use self::page_vm::AuthorizedCurrentPagePopupLoadEvent; pub(crate) use self::page_vm::AuthorizedCurrentPageRenderingUpdate; pub(crate) use self::page_vm::AuthorizedCurrentPageServiceWorkerClientMessage; diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index 2f43ba9704..608cbe61fc 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -131,6 +131,7 @@ mod page_owned_document_script_completion; mod page_owned_document_script_hooks; mod page_parser_async_module_admission; mod page_parser_owned_module_continuation; +mod page_popup_close; mod page_popup_load_event; mod page_rendering_update; #[cfg(test)] @@ -221,6 +222,7 @@ pub(crate) use page_module_reaction::AuthorizedCurrentPageModuleReaction; pub(crate) use page_modulepreload_start::AuthorizedCurrentChildModulepreloadStartTask; pub(crate) use page_navigation_api_task::AuthorizedCurrentPageNavigationApiTask; pub(crate) use page_opfs_task::AuthorizedCurrentPageOpfsTask; +pub(crate) use page_popup_close::AuthorizedCurrentPagePopupClose; pub(crate) use page_popup_load_event::AuthorizedCurrentPagePopupLoadEvent; pub(crate) use page_rendering_update::AuthorizedCurrentPageRenderingUpdate; pub(crate) use page_resource_completion::{ diff --git a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs index 5172b94cbd..58a0d21d05 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs @@ -33,6 +33,9 @@ impl PageVm { RendererPageDomManipulationTask::PopupLoadEvent(task) => self .apply_selected_page_popup_load_event_turn(task) .map(|outcome| outcome.map_action(PageDomManipulationTurnAction::PopupLoadEvent)), + RendererPageDomManipulationTask::PopupClose(task) => self + .apply_selected_page_popup_close_turn(task) + .map(|outcome| outcome.map_action(PageDomManipulationTurnAction::PopupClose)), RendererPageDomManipulationTask::ConnectedStyleEvent(task) => self .apply_selected_page_connected_style_event_turn(task) .map(|outcome| { diff --git a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs index 32938e4f46..36fc99d12c 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs @@ -37,6 +37,7 @@ impl PageVm { PageDomManipulationTurnAction::PopupLoadEvent(action) => { action.into_page_task_completion() } + PageDomManipulationTurnAction::PopupClose(action) => action.into_page_task_completion(), PageDomManipulationTurnAction::ConnectedStyleEvent(action) => { return self .finish_selected_page_connected_style_event_task(action, loader) diff --git a/moli-renderer-v8/src/runtime/page_vm/page_popup_close.rs b/moli-renderer-v8/src/runtime/page_vm/page_popup_close.rs new file mode 100644 index 0000000000..191e592b9b --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/page_popup_close.rs @@ -0,0 +1,71 @@ +use crate::page_task_queue::{ + PagePopupCloseTargetEffect, PagePopupCloseTurnAction, PagePopupCloseTurnOutcome, + RendererPagePopupCloseOwner, RendererPagePopupCloseTask, +}; + +use super::{IntoPageTaskCompletion, PageTaskCompletion, PageVm}; + +impl IntoPageTaskCompletion for PagePopupCloseTurnAction { + fn into_page_task_completion(self) -> PageTaskCompletion { + match self.target_effect { + PagePopupCloseTargetEffect::DefinitelyClosed => PageTaskCompletion::CheckpointOnly, + PagePopupCloseTargetEffect::DiscardedStaleOwner { .. } => { + PageTaskCompletion::NoCompletion + } + } + } +} + +/// Proof that the Page arbiter matched the PageVm namespace and a live +/// lightweight popup whose closing flag is set. +pub(crate) struct AuthorizedCurrentPagePopupClose(RendererPagePopupCloseTask); + +impl AuthorizedCurrentPagePopupClose { + fn new(task: RendererPagePopupCloseTask) -> Self { + Self(task) + } + + pub(crate) fn into_task(self) -> RendererPagePopupCloseTask { + self.0 + } +} + +impl PageVm { + fn current_page_popup_close_owner( + &self, + expected: RendererPagePopupCloseOwner, + ) -> Option { + if expected.root_document() != self.document_lifecycle.identity().document { + return None; + } + self.vm().current_popup_close_owner( + expected.popup_id(), + self.document_lifecycle.identity().document, + ) + } + + pub(in crate::runtime) fn apply_selected_page_popup_close_turn( + &mut self, + task: RendererPagePopupCloseTask, + ) -> anyhow::Result { + let owner = task.owner(); + let current_owner = self.current_page_popup_close_owner(owner); + let target_effect = if current_owner == Some(owner) { + self.vm_mut() + .apply_current_popup_close_body(AuthorizedCurrentPagePopupClose::new(task))?; + PagePopupCloseTargetEffect::DefinitelyClosed + } else { + tracing::debug!( + ?owner, + ?current_owner, + "discarded stale exact-popup close task" + ); + PagePopupCloseTargetEffect::DiscardedStaleOwner { current_owner } + }; + let action = PagePopupCloseTurnAction { + owner, + target_effect, + }; + Ok(PagePopupCloseTurnOutcome::new(action)) + } +} diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 806a15ba8e..64a4880912 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -746,6 +746,7 @@ mod page_task_enqueue; mod parser_owned_classic; pub(crate) use parser_owned_classic::*; mod parser_module_terminal; +mod popup_close; mod popup_load_event; mod post_parse; mod post_parse_lifecycle; diff --git a/moli-renderer-v8/src/script_vm/popup_close.rs b/moli-renderer-v8/src/script_vm/popup_close.rs new file mode 100644 index 0000000000..e535015dbf --- /dev/null +++ b/moli-renderer-v8/src/script_vm/popup_close.rs @@ -0,0 +1,35 @@ +use anyhow::Result; + +use super::ScriptVm; +use crate::{ + page_task_queue::RendererPagePopupCloseOwner, + runtime::{AuthorizedCurrentPagePopupClose, RendererDocumentToken}, +}; + +impl ScriptVm { + pub(crate) fn current_popup_close_owner( + &self, + popup_id: u64, + root_document: RendererDocumentToken, + ) -> Option { + self._context_host + .borrow() + .lightweight_popup_is_closing(popup_id) + .then(|| RendererPagePopupCloseOwner::new(root_document, popup_id)) + } + + pub(crate) fn apply_current_popup_close_body( + &mut self, + authorization: AuthorizedCurrentPagePopupClose, + ) -> Result<()> { + let popup_id = authorization.into_task().owner().popup_id(); + self.with_default_context_scope(|scope, host_ptr| { + assert!( + unsafe { &mut *host_ptr } + .definitely_close_lightweight_popup_browsing_context(scope, popup_id), + "authorized popup close task must retain its closing browsing context" + ); + Ok(()) + }) + } +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs index 657638bbfc..5541d19ce0 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs @@ -5885,7 +5885,8 @@ popup.clearTimeout(cancelledPopupTimer); #[tokio::test] async fn lightweight_popup_close_retires_its_local_window_timers() { let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); - let mut vm = new_storage_test_vm_with_loader("https://timer-popup-close.test/", &loader); + let mut vm = + new_page_task_executor_test_vm_with_loader("https://timer-popup-close.test/", &loader); vm.eval( r#" @@ -5897,9 +5898,14 @@ popup.close(); "#, ) .expect("popup timer close setup should evaluate"); - vm.advance_timers_until_deadline_for_test(&loader) - .await - .expect("closed popup timer should leave the timer queue quiescent"); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(popup.opener === null)", + "true", + "queued popup definite close", + ) + .await; assert_eq!( vm.eval("__closedPopupTimerEvents.length") .expect("closed popup timer result should evaluate"), diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 0f351a5a36..dc8de20844 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -25606,9 +25606,10 @@ fn window_open_about_blank_returns_lightweight_popup_window() { ); } -#[test] -fn lightweight_popup_opener_accessor_preserves_and_disowns_the_underlying_relation() { - let mut vm = new_storage_test_vm("https://example.com/"); +#[tokio::test] +async fn lightweight_popup_opener_accessor_preserves_and_disowns_the_underlying_relation() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_page_task_executor_test_vm_with_loader("https://example.com/", &loader); let result = vm .eval( @@ -25631,6 +25632,8 @@ fn lightweight_popup_opener_accessor_preserves_and_disowns_the_underlying_relati const closedGet = Object.getOwnPropertyDescriptor(closed, "opener").get; closed.close(); closed.opener = "closed replacement"; + globalThis.__closedPopupOpenerGet = closedGet; + globalThis.__closedPopupForOpenerTest = closed; return JSON.stringify({ replaced: { @@ -25655,8 +25658,9 @@ fn lightweight_popup_opener_accessor_preserves_and_disowns_the_underlying_relati boundGetterIsNull: disownedGet() === null, accessorPreserved: descriptorAfterNull.get === disownedGet }, - closed: { - boundGetterIsNull: closedGet() === null, + closing: { + closedFlag: closed.closed, + boundGetterKeepsRelation: closedGet() === window, value: closed.opener } }); @@ -25667,7 +25671,20 @@ fn lightweight_popup_opener_accessor_preserves_and_disowns_the_underlying_relati assert_eq!( result, - r#"{"replaced":{"accessorShape":"function:function::true:true","value":"replacement","boundGetterKeepsRelation":true,"borrowedGetterKeepsRelation":true,"dataShape":"true:true:true"},"disowned":{"valueIsNull":true,"boundGetterIsNull":true,"accessorPreserved":true},"closed":{"boundGetterIsNull":true,"value":"closed replacement"}}"# + r#"{"replaced":{"accessorShape":"function:function::true:true","value":"replacement","boundGetterKeepsRelation":true,"borrowedGetterKeepsRelation":true,"dataShape":"true:true:true"},"disowned":{"valueIsNull":true,"boundGetterIsNull":true,"accessorPreserved":true},"closing":{"closedFlag":true,"boundGetterKeepsRelation":true,"value":"closed replacement"}}"# + ); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__closedPopupOpenerGet() === null)", + "true", + "queued popup definite close clears the opener relation", + ) + .await; + assert_eq!( + vm.eval("String(__closedPopupForOpenerTest.opener)") + .expect("closed popup replacement opener should evaluate"), + "closed replacement" ); } @@ -26973,6 +26990,129 @@ async fn noopener_hyperlink_reuses_an_existing_named_popup_and_preserves_its_ope ); } +#[tokio::test] +async fn top_realm_popup_load_handler_can_queue_a_timer_after_window_close() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = + new_page_task_executor_test_vm_with_loader("https://example.com/base/page.html", &loader); + + assert_eq!( + vm.eval( + r#" +(() => { + globalThis.__popupCloseHandlerState = "pending"; + globalThis.__popupCloseHandlerEvents = []; + const targetUrl = URL.createObjectURL(new Blob([ + "close target" + ], { type: "text/html" })); + const popup = open(targetUrl); + popup.onload = () => { + popup.close(); + const timerId = setTimeout(() => { + __popupCloseHandlerEvents.push([ + popup.closed, + popup.opener === null + ].join(":")); + }, 0); + __popupCloseHandlerState = [ + popup.closed, + popup.opener === window, + timerId !== 0 + ].join(":"); + }; + globalThis.__popupCloseHandlerPopup = popup; + return __popupCloseHandlerState; +})() +"#, + ) + .expect("popup close handler setup should evaluate"), + "pending" + ); + + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "__popupCloseHandlerState", + "true:true:true", + "popup close handler synchronous state", + ) + .await; + assert_eq!( + vm.eval("__popupCloseHandlerEvents.length") + .expect("popup close handler timer count should evaluate"), + "0", + "the close task must remain ahead of the later timer" + ); + + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "__popupCloseHandlerEvents.join('|')", + "true:true", + "top-realm timer queued after popup close", + ) + .await; +} + +#[tokio::test] +async fn named_popup_broadcast_precedes_its_queued_close() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = + new_page_task_executor_test_vm_with_loader("https://example.com/base/page.html", &loader); + + assert_eq!( + vm.eval( + r#" +(() => { + const popupName = "moli-noopener-close-order"; + globalThis.__namedPopupCloseOrder = "pending"; + const channel = new BroadcastChannel(popupName); + const popup = open("about:blank", popupName); + channel.onmessage = event => { + __namedPopupCloseOrder = JSON.stringify({ + payload: event.data, + openerPreserved: popup.opener === window, + alreadyClosing: popup.closed + }); + }; + const targetMarkup = `