fix(window): defer lightweight popup teardown

This commit is contained in:
ldm0
2026-09-14 14:40:22 +08:00
parent 85d7f2a926
commit 5daea94264
18 changed files with 825 additions and 45 deletions
@@ -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
},
)
@@ -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<v8::Global<v8::Value>> {
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::<v8::Object>::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<crate::native_bridge::WindowExecutionContextIdentity>,
mut invocation: CallbackInvocation<'s, '_>,
) -> Option<v8::Global<v8::Value>> {
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);
@@ -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<WindowExecutionContextIdentity>,
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<WindowExecutionContextIdentity>,
is_callable: bool,
}
@@ -133,6 +139,10 @@ impl<'s> SimpleObjectEventListenerSnapshot<'s> {
self.relevant_context
}
pub(crate) fn relevant_identity(&self) -> Option<WindowExecutionContextIdentity> {
self.relevant_identity
}
pub(crate) fn callable_function(&self) -> Option<v8::Local<'s, v8::Function>> {
self.is_callable
.then(|| unsafe { v8::Local::<v8::Function>::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::<v8::Object>::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<WindowExecutionContextIdentity>,
) {
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<WindowExecutionContextIdentity>,
) {
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<WindowExecutionContextIdentity>,
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<WindowExecutionContextIdentity>,
) -> 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<WindowExecutionContextIdentity>,
) {
let value = identity
.and_then(|identity| v8_string(scope, &identity.serialize_for_internal_slot()))
.map(v8::Local::<v8::Value>::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,
@@ -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 {
@@ -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<DocumentSandboxPolicy>,
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<LightweightPopupCloseTransition> {
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<v8::Local<'s, v8::Object>> {
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<u64> {
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,
@@ -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<Self> {
fn access_policy(value: &str) -> Option<WindowExecutionContextAccessPolicy> {
match value {
"0" => Some(WindowExecutionContextAccessPolicy::EnforceWebOrigin),
"1" => Some(WindowExecutionContextAccessPolicy::Universal),
_ => None,
}
}
let parts = serialized.split(':').collect::<Vec<_>>();
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::<u64>().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
);
}
}
+5
View File
@@ -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,
@@ -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)
}
@@ -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<RendererPagePopupCloseOwner>,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct PagePopupCloseTurnAction {
pub(crate) owner: RendererPagePopupCloseOwner,
pub(crate) target_effect: PagePopupCloseTargetEffect,
}
pub(crate) type PagePopupCloseTurnOutcome = PageOwnerTurnOutcome<PagePopupCloseTurnAction>;
+1
View File
@@ -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;
@@ -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::{
@@ -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| {
@@ -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)
@@ -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<RendererPagePopupCloseOwner> {
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<PagePopupCloseTurnOutcome> {
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))
}
}
+1
View File
@@ -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;
@@ -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<RendererPagePopupCloseOwner> {
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(())
})
}
}
@@ -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"),
@@ -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([
"<!doctype html><title>close target</title>"
], { 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 = `<!doctype html><script>
const channel = new BroadcastChannel(window.name);
channel.postMessage("sent-before-close");
window.close();
<\/script>`;
const link = document.createElement("a");
link.rel = "noopener";
link.target = popupName;
link.href = URL.createObjectURL(new Blob([targetMarkup], { type: "text/html" }));
document.body.appendChild(link);
globalThis.__namedPopupCloseOrderPopup = popup;
link.click();
return __namedPopupCloseOrder;
})()
"#,
)
.expect("named popup close ordering setup should evaluate"),
"pending"
);
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"__namedPopupCloseOrder",
r#"{"payload":"sent-before-close","openerPreserved":true,"alreadyClosing":true}"#,
"BroadcastChannel delivery before named popup close",
)
.await;
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__namedPopupCloseOrderPopup.opener === null)",
"true",
"named popup definite close",
)
.await;
}
#[tokio::test]
async fn lightweight_popup_post_message_interleaves_opener_promise_waiters() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");