diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/custom_elements_registry.rs b/moli-renderer-v8/src/context_bootstrap/constructors/custom_elements_registry.rs index cabbb60886..c394805c0d 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/custom_elements_registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/custom_elements_registry.rs @@ -373,7 +373,7 @@ pub(in crate::context_bootstrap) fn custom_elements_initialize_callback<'s>( scope.throw_exception(exception); return; }; - if !custom_elements::registry_association_matches_document_default( + if !custom_elements::registry_association_matches_document( unsafe { &*host_ptr }, root_document, registry_association, diff --git a/moli-renderer-v8/src/custom_elements.rs b/moli-renderer-v8/src/custom_elements.rs index 506e3b4afa..f66ae2f05c 100644 --- a/moli-renderer-v8/src/custom_elements.rs +++ b/moli-renderer-v8/src/custom_elements.rs @@ -132,13 +132,13 @@ pub(crate) use registry_clone_retarget::{ }; mod registry_install; pub(crate) use registry_install::{ - build_custom_elements_registry_for_window, rebind_materialized_child_custom_elements_registry, + build_custom_elements_registry_for_window, custom_elements_registry_for_current_realm, + rebind_materialized_child_custom_elements_registry, }; mod registry_runtime; pub(crate) use registry_runtime::{ mark_scoped_custom_elements_registry, registry_association_from_create_options_value, - registry_association_from_value, registry_association_matches_document_default, - registry_store_key, + registry_association_from_value, registry_association_matches_document, registry_store_key, }; mod registry_initializer; pub(crate) use registry_initializer::initialize_registry_for_subtree; diff --git a/moli-renderer-v8/src/custom_elements/registry_install.rs b/moli-renderer-v8/src/custom_elements/registry_install.rs index ea23323e5d..7f992a03d7 100644 --- a/moli-renderer-v8/src/custom_elements/registry_install.rs +++ b/moli-renderer-v8/src/custom_elements/registry_install.rs @@ -3,10 +3,11 @@ use crate::web_api_interfaces; use crate::{ context_bootstrap::{WindowLazySurface, rematerialize_window_lazy_surface_if_cached}, document_runtime::DomHandle, - util::{get_private_value, set_private_value, v8str}, + util::{get_private_value, set_private_value}, }; use anyhow::Result; use moli_webapi_declare::WebApiObject; +use std::rc::Rc; #[derive(WebApiObject)] #[webapi(interface = web_api_interfaces::CustomElementRegistry, prototype = "Object")] @@ -16,6 +17,55 @@ struct CustomElementsRegistryDeclaration<'scope> { } const CUSTOM_ELEMENTS_WINDOW_OWNER_CHILD_SLOT: &str = "__moliCustomElementsWindowOwnerChild"; +const WINDOW_REGISTRY_CACHE_SLOT: &str = "__moliWindowCustomElementsRegistries"; + +struct WindowRegistryCache(v8::Weak); + +fn window_registry_cache_key<'s>( + scope: &mut v8::PinScope<'s, '_>, + child_handle: Option, +) -> v8::Local<'s, v8::Value> { + child_handle + .map(|handle| v8::BigInt::new_from_u64(scope, handle.index() as u64).into()) + .unwrap_or_else(|| v8::undefined(scope).into()) +} + +pub(crate) fn custom_elements_registry_for_current_realm<'s>( + scope: &mut v8::PinScope<'s, '_>, + child_handle: Option, +) -> Result> { + let context = scope.get_current_context(); + let cached = context + .get_slot::() + .and_then(|cache| cache.0.to_local(scope)); + let cache = if let Some(cache) = cached { + cache + } else { + let prototype = crate::context_bootstrap::ensure_intrinsic_interface_prototype( + scope, + "CustomElementRegistry", + )?; + let cache = v8::Map::new(scope); + // The realm traces the cache through its intrinsic prototype. The + // native slot is weak so it does not add a root retaining the realm. + // WindowProxy slots cannot serve this role after detach_global(). + set_private_value(scope, prototype, WINDOW_REGISTRY_CACHE_SLOT, cache.into()); + let _ = context.set_slot(Rc::new(WindowRegistryCache(v8::Weak::new(scope, cache)))); + cache + }; + let key = window_registry_cache_key(scope, child_handle); + if let Some(registry) = cache + .get(scope, key) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + return Ok(registry); + } + let registry = new_custom_elements_registry_for_owner(scope, child_handle)?; + cache + .set(scope, key, registry.into()) + .ok_or_else(|| anyhow::anyhow!("failed to cache the Window custom element registry"))?; + Ok(registry) +} pub(crate) fn build_custom_elements_registry_for_window<'s>( scope: &mut v8::PinScope<'s, '_>, @@ -24,7 +74,7 @@ pub(crate) fn build_custom_elements_registry_for_window<'s>( let child_handle = custom_elements_owner_child(scope, window).or_else(|| { crate::context_bootstrap::child_browsing_context_handle_for_current_realm_scope(scope) }); - new_custom_elements_registry_for_owner(scope, child_handle) + custom_elements_registry_for_current_realm(scope, child_handle) } pub(crate) fn rebind_materialized_child_custom_elements_registry<'s>( @@ -59,6 +109,14 @@ fn rebind_materialized_child_custom_elements_registry_in_current_realm<'s>( CUSTOM_ELEMENTS_WINDOW_OWNER_CHILD_SLOT, handle_value.into(), ); + if let Some(cache) = scope + .get_current_context() + .get_slot::() + .and_then(|cache| cache.0.to_local(scope)) + { + let key = window_registry_cache_key(scope, Some(child_handle)); + let _ = cache.delete(scope, key); + } rematerialize_window_lazy_surface_if_cached(scope, window, WindowLazySurface::CustomElements)?; Ok(()) } @@ -79,8 +137,7 @@ fn new_custom_elements_registry_for_owner<'s>( scope: &mut v8::PinScope<'s, '_>, child_handle: Option, ) -> Result> { - let global = scope.get_current_context().global(scope); - let registry = new_custom_elements_registry(scope, global)?; + let registry = new_custom_elements_registry(scope)?; let Some(child_handle) = child_handle else { return Ok(registry); }; @@ -96,16 +153,11 @@ fn new_custom_elements_registry_for_owner<'s>( fn new_custom_elements_registry<'s>( scope: &mut v8::PinScope<'s, '_>, - global: v8::Local<'s, v8::Object>, ) -> Result> { - let ctor = global - .get(scope, v8str(scope, "CustomElementRegistry").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - .ok_or_else(|| anyhow::anyhow!("failed to load CustomElementRegistry constructor"))?; - let prototype = ctor - .get(scope, v8str(scope, "prototype").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - .ok_or_else(|| anyhow::anyhow!("failed to load CustomElementRegistry prototype"))?; + let prototype = crate::context_bootstrap::ensure_intrinsic_interface_prototype( + scope, + "CustomElementRegistry", + )?; CustomElementsRegistryDeclaration::new(prototype) .bind(scope) .map_err(anyhow::Error::from) diff --git a/moli-renderer-v8/src/custom_elements/registry_runtime.rs b/moli-renderer-v8/src/custom_elements/registry_runtime.rs index 087ce8e654..0a4fb2754a 100644 --- a/moli-renderer-v8/src/custom_elements/registry_runtime.rs +++ b/moli-renderer-v8/src/custom_elements/registry_runtime.rs @@ -70,14 +70,15 @@ pub(crate) fn registry_association_from_create_options_value<'s>( registry_association_from_value(scope, registry) } -pub(crate) fn registry_association_matches_document_default( +pub(crate) fn registry_association_matches_document( host: &JsContextHost, document_handle: DomHandle, association: CustomElementRegistryAssociation, ) -> bool { if association.is_document_default_backed_registry() { - return host.default_custom_element_registry_association_for_document(document_handle) - == association; + // A Document keeps its registry after its browsing context is removed. + // Validate against that association, not a default inferred from live frames. + return host.effective_custom_element_registry_association(document_handle) == association; } true } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs index 5b0a427c04..c17be09444 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs @@ -134,11 +134,15 @@ impl JsContextHost { return Ok(context); } if let Some(stale) = pending_contexts.borrow_mut().remove(&handle) { + let stale_context = v8::Local::new(scope, &stale.context); + { + let stale_scope = &mut v8::ContextScope::new(scope, stale_context); + crate::native_bridge::clear_context_wrapper_cache_for_teardown(stale_scope, false); + } self.retire_window_execution_contexts_for_context_token( stale.runtime_observable_context_token, config.resource_owner_id, ); - let stale_context = v8::Local::new(scope, &stale.context); stale_context.detach_global(); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs index 217b5915a5..469d3d4ea8 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs @@ -557,6 +557,7 @@ impl JsContextHost { None } None => { + let document_handle = self.child_browsing_context_document_handle(handle); self.cancel_child_meta_refresh_navigation(handle); self.clear_pending_child_document_loads_for_handle(handle); self.unregister_service_worker_child_client(handle); @@ -564,7 +565,14 @@ impl JsContextHost { self.remove_child_browsing_context_entry(handle); self.detach_child_frame_owner_and_wake_parent(handle); self.clear_live_child_window_proxy_records(handle); - self.clear_custom_element_registry_associations_for_child_context(handle); + if let Some(document_handle) = document_handle { + self.set_custom_element_registry_association( + document_handle, + CustomElementRegistryAssociation::Registry( + CustomElementRegistryKey::Child(handle), + ), + ); + } self.child_custom_elements.remove(&handle); self.clear_child_window_event_listeners(handle); self.close_broadcast_channels_for_child_context(handle); @@ -711,7 +719,9 @@ impl JsContextHost { self.queue_child_frame_detachment_event(frame_id); } self.clear_live_child_window_proxy_records(handle); - self.clear_custom_element_registry_associations_for_child_context(handle); + // Registry associations belong to retained DOM nodes, not to the + // execution context being retired. Snapshot the document default + // before subsequent lookups can no longer infer it from the frame. if let Some(document_handle) = document_handle_before_drop { self.set_custom_element_registry_association( document_handle, @@ -796,25 +806,15 @@ impl JsContextHost { for handle in stale_shared_worker_client_handles { self.disconnect_shared_worker_clients_for_child_context(handle); } - let mut stale_registry_context_handles = self + let stale_registry_context_handles = self .child_browsing_context_document_handles .keys() .chain(self.child_custom_elements.keys()) .copied() .filter(|handle| !live_handles.contains(handle)) .collect::>(); - for association in self.custom_element_registry_associations.values() { - if let CustomElementRegistryAssociation::Registry(CustomElementRegistryKey::Child( - handle, - )) = association - && !live_handles.contains(handle) - { - stale_registry_context_handles.insert(*handle); - } - } for handle in stale_registry_context_handles { let document_handle = self.child_browsing_context_document_handle(handle); - self.clear_custom_element_registry_associations_for_child_context(handle); if let Some(document_handle) = document_handle { self.set_custom_element_registry_association( document_handle, 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 15cbfa0012..c604c1a742 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -2008,17 +2008,10 @@ impl JsContextHost { let Some(document_handle) = self.dom_host().owner_document_handle(handle) else { return CustomElementRegistryAssociation::Null; }; - if let Some(child_handle) = - self.child_browsing_context_host_for_document_handle(document_handle) - { - return CustomElementRegistryAssociation::Registry(CustomElementRegistryKey::Child( - child_handle, - )); + if handle != document_handle { + return self.effective_custom_element_registry_association(document_handle); } - if document_handle == self.dom_host().document_handle() { - return CustomElementRegistryAssociation::Registry(CustomElementRegistryKey::Global); - } - CustomElementRegistryAssociation::Null + self.default_custom_element_registry_association_for_document(document_handle) } fn normalize_explicit_custom_element_registry_association( @@ -2037,8 +2030,11 @@ impl JsContextHost { let Some(document_handle) = self.dom_host().owner_document_handle(handle) else { return association; }; - let owner_default = - self.default_custom_element_registry_association_for_document(document_handle); + let owner_default = if handle == document_handle { + self.default_custom_element_registry_association_for_document(document_handle) + } else { + self.effective_custom_element_registry_association(document_handle) + }; if owner_default == CustomElementRegistryAssociation::Registry(CustomElementRegistryKey::Global) { @@ -2099,6 +2095,15 @@ impl JsContextHost { ) -> Option> { match self.effective_custom_element_registry_association(handle) { CustomElementRegistryAssociation::Null => Some(v8::null(scope).into()), + CustomElementRegistryAssociation::Registry(key) if key.is_document_default_backed() => { + let document_handle = self.dom_host().owner_document_handle(handle)?; + let host_ptr = self as *mut Self; + let document = self.bridge.wrap_handle(scope, host_ptr, document_handle)?; + let context = document.get_creation_context(scope)?; + let scope = &mut v8::ContextScope::new(scope, context); + self.custom_element_registry_object_for_key(scope, key) + .map(Into::into) + } CustomElementRegistryAssociation::Registry(key) => self .custom_element_registry_object_for_key(scope, key) .map(Into::into), @@ -2112,18 +2117,14 @@ impl JsContextHost { ) -> Option> { match key { CustomElementRegistryKey::Global => { - let global = scope.get_current_context().global(scope); - global - .get(scope, crate::util::v8str(scope, "customElements").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) + crate::custom_elements::custom_elements_registry_for_current_realm(scope, None).ok() } CustomElementRegistryKey::Child(handle) => { - let window = self - .child_browsing_context_window_wrapper(scope, handle) - .or_else(|| self.cached_detached_iframe_content_window(scope, handle))?; - window - .get(scope, crate::util::v8str(scope, "customElements").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) + crate::custom_elements::custom_elements_registry_for_current_realm( + scope, + Some(handle), + ) + .ok() } CustomElementRegistryKey::Scoped(id) => { let registry = self diff --git a/moli-renderer-v8/src/native_bridge/document/custom_element_options.rs b/moli-renderer-v8/src/native_bridge/document/custom_element_options.rs index 0a1fea83fa..bcbcdc05c6 100644 --- a/moli-renderer-v8/src/native_bridge/document/custom_element_options.rs +++ b/moli-renderer-v8/src/native_bridge/document/custom_element_options.rs @@ -12,7 +12,7 @@ pub(in crate::native_bridge) fn validate_registry_association_for_document( let Some(registry_association) = registry_association else { return true; }; - if custom_elements::registry_association_matches_document_default( + if custom_elements::registry_association_matches_document( unsafe { &*runtime_ptr }, document_handle, registry_association, diff --git a/moli-renderer-v8/src/native_bridge/identity.rs b/moli-renderer-v8/src/native_bridge/identity.rs index 8937419fd2..6cec2f43c5 100644 --- a/moli-renderer-v8/src/native_bridge/identity.rs +++ b/moli-renderer-v8/src/native_bridge/identity.rs @@ -433,11 +433,16 @@ impl StaticHandleCollectionStore { struct BridgeContextWrapperCache { wrappers: DenseReflectorMap, live_collection_wrappers: HashMap, + retired_wrappers: HashMap>, + retired_live_collection_wrappers: HashMap>, } #[derive(Debug)] struct SharedDefaultWorldWrapperCache; +#[derive(Debug)] +struct RetiredWrapperContext; + #[derive(Debug)] struct BridgeCachedWrapper { wrapper: v8::Global, @@ -533,17 +538,51 @@ pub(crate) fn clear_context_wrapper_cache_for_teardown( include_shared_default_world: bool, ) { let context = scope.get_current_context(); - if !include_shared_default_world - && context - .get_slot::() - .is_some() - { - return; - } if let Some(cache) = context.get_slot::>() { let mut cache = cache.borrow_mut(); - cache.wrappers.clear(); - cache.live_collection_wrappers.clear(); + if include_shared_default_world { + cache.wrappers.clear(); + cache.live_collection_wrappers.clear(); + cache.retired_wrappers.clear(); + cache.retired_live_collection_wrappers.clear(); + return; + } + let shared = context + .get_slot::() + .is_some(); + let realm = context + .get_slot::() + .as_deref() + .and_then(|token| NonZeroU64::new(token.as_u64())); + let _ = context.set_slot(Rc::new(RetiredWrapperContext)); + // Retiring execution must release strong roots without replacing a DOM + // object that is still reachable from JavaScript in another realm. + cache + .retired_wrappers + .retain(|_, wrapper| !wrapper.is_empty()); + cache + .retired_live_collection_wrappers + .retain(|_, wrapper| !wrapper.is_empty()); + let mut retired = Vec::new(); + cache.wrappers.retain(|id, entry| { + if shared && entry.creation_realm != realm { + return true; + } + retired.push((id, v8::Weak::new(scope, &entry.wrapper))); + false + }); + cache.retired_wrappers.extend(retired); + let mut retired_collections = Vec::new(); + cache.live_collection_wrappers.retain(|descriptor, entry| { + if shared && entry.creation_realm != realm { + return true; + } + retired_collections.push((descriptor.clone(), v8::Weak::new(scope, &entry.wrapper))); + false + }); + cache + .retired_live_collection_wrappers + .extend(retired_collections); } } @@ -596,6 +635,13 @@ impl BridgeIdentityStore { .wrappers .get(&reflector_id) .map(|entry| v8::Local::new(scope, &entry.wrapper)) + .or_else(|| { + context_wrapper_cache(scope) + .borrow() + .retired_wrappers + .get(&reflector_id) + .and_then(|wrapper| wrapper.to_local(scope)) + }) } pub(super) fn cache_wrapper( @@ -608,10 +654,21 @@ impl BridgeIdentityStore { set_context_window_wrapper(scope, wrapper); return; } - context_wrapper_cache(scope) - .borrow_mut() - .wrappers - .insert(reflector_id, BridgeCachedWrapper::new(scope, wrapper)); + let cache = context_wrapper_cache(scope); + let mut cache = cache.borrow_mut(); + if scope + .get_current_context() + .get_slot::() + .is_some() + { + cache + .retired_wrappers + .insert(reflector_id, v8::Weak::new(scope, wrapper)); + } else { + cache + .wrappers + .insert(reflector_id, BridgeCachedWrapper::new(scope, wrapper)); + } } pub(super) fn register_live_collection(&mut self, descriptor: LiveCollectionDescriptor) -> u32 { @@ -635,6 +692,13 @@ impl BridgeIdentityStore { .live_collection_wrappers .get(descriptor) .map(|entry| v8::Local::new(scope, &entry.wrapper)) + .or_else(|| { + context_wrapper_cache(scope) + .borrow() + .retired_live_collection_wrappers + .get(descriptor) + .and_then(|wrapper| wrapper.to_local(scope)) + }) } pub(super) fn cache_live_collection_wrapper( @@ -643,10 +707,21 @@ impl BridgeIdentityStore { descriptor: LiveCollectionDescriptor, wrapper: v8::Local<'_, v8::Object>, ) { - context_wrapper_cache(scope) - .borrow_mut() - .live_collection_wrappers - .insert(descriptor, BridgeCachedWrapper::new(scope, wrapper)); + let cache = context_wrapper_cache(scope); + let mut cache = cache.borrow_mut(); + if scope + .get_current_context() + .get_slot::() + .is_some() + { + cache + .retired_live_collection_wrappers + .insert(descriptor, v8::Weak::new(scope, wrapper)); + } else { + cache + .live_collection_wrappers + .insert(descriptor, BridgeCachedWrapper::new(scope, wrapper)); + } } pub(super) fn retire_default_world_wrappers_for_realm( diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 4ddc1871f3..676acff8e7 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -3652,13 +3652,14 @@ impl ScriptVm { Some(context) } Some(context) => { + let context_ptr = &context.context as *const v8::Global; + self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false); self._context_host .borrow_mut() .retire_window_execution_contexts_for_context_token( context.runtime_observable_context_token, self.resource_owner_id, ); - let context_ptr = &context.context as *const v8::Global; self.renderer_document_isolate .with_entered_renderer_document_isolate(|isolate| { let scope = pin!(v8::HandleScope::new(isolate)); @@ -3788,6 +3789,9 @@ impl ScriptVm { .collect::>() }; if !stale_prebootstrapped_contexts.is_empty() { + for context in &stale_prebootstrapped_contexts { + self.clear_context_wrapper_cache_for_context_ptr(&context.context, false); + } { let mut host = self._context_host.borrow_mut(); for context in &stale_prebootstrapped_contexts { @@ -3837,6 +3841,8 @@ impl ScriptVm { let Some(context) = self.child_frame_realm_store.remove(&execution_context_id) else { return; }; + let context_ptr: *const v8::Global = &context.context as *const _; + self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false); let retired_timer_count = self .document_runtime .cancel_timers_for_context_token(context.runtime_observable_context_token); @@ -3912,8 +3918,6 @@ impl ScriptVm { retired_timer_count, "retired child Runtime binding context" ); - let context_ptr: *const v8::Global = &context.context as *const _; - self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false); assert!( self.page_inspector .destroy_context_registration(context.inspector_context_registration_id), diff --git a/moli-renderer-v8/src/script_vm/child_document_modulator.rs b/moli-renderer-v8/src/script_vm/child_document_modulator.rs index 0fb12882f8..492e1d4840 100644 --- a/moli-renderer-v8/src/script_vm/child_document_modulator.rs +++ b/moli-renderer-v8/src/script_vm/child_document_modulator.rs @@ -83,6 +83,19 @@ impl ScriptVm { .context_id_for_local_window_id(retired_owner.local_window_id) { self.destroy_child_default_context(execution_context_id); + } else if execution_context_retired { + let pending_context = self + .prebootstrapped_child_default_contexts + .borrow() + .values() + .find(|context| context.local_window_id == retired_owner.local_window_id) + .map(|context| context.context.clone()); + if let Some(context) = pending_context { + // A synchronously exposed realm can retire before its + // FrameRealm is registered. Preserve reachable wrappers + // before retiring the execution-context binding below. + self.clear_context_wrapper_cache_for_context_ptr(&context, false); + } } let retired_timer_count = if execution_context_retired { self.document_runtime diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements.rs index 71d08f2481..f8ccb0de1a 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements.rs @@ -1,6 +1,7 @@ use super::*; use crate::custom_elements::CustomElementRegistryKey; +mod inactive_document; mod insertion; mod parser; mod upgrade_reentry; diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements/inactive_document.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements/inactive_document.rs new file mode 100644 index 0000000000..c2f8133fc7 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements/inactive_document.rs @@ -0,0 +1,278 @@ +use super::*; + +fn inactive_document_test_vm(loaded_srcdoc: bool) -> StandaloneScriptVmHarness { + let mut vm = new_storage_test_vm("https://inactive-document-registry.test/"); + vm.eval( + r#" + const root = document.documentElement || document.appendChild(document.createElement('html')); + const body = document.body || root.appendChild(document.createElement('body')); + globalThis.registryFrame = document.createElement('iframe'); + body.appendChild(registryFrame); + "#, + ) + .unwrap(); + if loaded_srcdoc { + vm.eval("registryFrame.srcdoc = ''") + .unwrap(); + vm.drain_pending_child_frame_work_for_test(); + } + vm +} + +#[test] +fn inactive_child_document_accepts_its_registry_without_reviving_script_execution() { + for loaded_srcdoc in [false, true] { + let mut vm = inactive_document_test_vm(loaded_srcdoc); + let result = vm + .eval( + r#" + (() => { + globalThis.retainedDocument = registryFrame.contentDocument; + globalThis.retainedRegistry = registryFrame.contentWindow.customElements; + globalThis.inactiveScriptRuns = 0; + registryFrame.remove(); + const failures = []; + const check = (name, operation) => { + try { if (!operation()) failures.push(name); } + catch (error) { failures.push(name + ':' + error.name); } + }; + for (const name of ['div', 'script', 'unregistered-element']) { + for (const explicit of [false, true]) { + const options = explicit ? {customElementRegistry: retainedRegistry} : undefined; + check(name + ':create:' + explicit, () => + retainedDocument.createElement(name, options).ownerDocument === retainedDocument); + check(name + ':createNS:' + explicit, () => + retainedDocument.createElementNS('http://www.w3.org/1999/xhtml', name, options) + .ownerDocument === retainedDocument); + } + } + const script = retainedDocument.createElement('script'); + script.textContent = 'parent.inactiveScriptRuns++'; + retainedDocument.body.appendChild(script); + check('script stayed inactive', () => inactiveScriptRuns === 0); + check('frame stayed detached', () => registryFrame.contentDocument === null); + return JSON.stringify(failures); + })() + "#, + ) + .unwrap(); + assert_eq!(result, "[]", "loaded_srcdoc={loaded_srcdoc}"); + vm.drain_pending_child_frame_work_for_test(); + assert_eq!(vm.eval("inactiveScriptRuns").unwrap(), "0"); + assert_eq!( + vm.eval("retainedDocument.createElement('span').ownerDocument === retainedDocument") + .unwrap(), + "true", + "the retained Document must still own later-created elements" + ); + assert_eq!( + vm.eval("retainedDocument.createElement('span', {customElementRegistry: retainedRegistry}).ownerDocument === retainedDocument") + .unwrap(), + "true", + "later creation must still accept the Document's original registry" + ); + } +} + +#[test] +fn inactive_child_document_registry_getter_preserves_later_registry_validation() { + let mut vm = inactive_document_test_vm(false); + let result = vm + .eval( + r#" + (() => { + const doc = registryFrame.contentDocument; + const registry = registryFrame.contentWindow.customElements; + const scoped = new CustomElementRegistry(); + registryFrame.remove(); + const failures = []; + const check = (name, operation) => { + try { if (!operation()) failures.push(name); } + catch (error) { failures.push(name + ':' + error.name); } + }; + check('registry getter', () => doc.customElementRegistry === registry); + check('implicit registry', () => doc.createElement('div').ownerDocument === doc); + check('explicit registry', () => + doc.createElement('div', {customElementRegistry: registry}).ownerDocument === doc); + check('explicit null', () => + doc.createElement('div', {customElementRegistry: null}).customElementRegistry === null); + check('scoped registry', () => + doc.createElement('div', {customElementRegistry: scoped}).customElementRegistry === scoped); + check('importNode', () => + doc.importNode(document.createElement('span'), {customElementRegistry: registry}) + .ownerDocument === doc); + check('attachShadow', () => + doc.createElement('div').attachShadow({mode: 'open', customElementRegistry: registry}) + .customElementRegistry === registry); + const rejectsForeign = operation => { + try { operation(); return false; } + catch (error) { return error.name === 'NotSupportedError'; } + }; + check('reject parent registry', () => rejectsForeign(() => + doc.createElement('div', {customElementRegistry: customElements}))); + check('reject child registry in parent', () => rejectsForeign(() => + document.createElement('div', {customElementRegistry: registry}))); + check('reject foreign registry in createElementNS', () => rejectsForeign(() => + doc.createElementNS('http://www.w3.org/1999/xhtml', 'div', + {customElementRegistry: customElements}))); + check('getter remains stable', () => doc.customElementRegistry === registry); + return JSON.stringify(failures); + })() + "#, + ) + .unwrap(); + assert_eq!(result, "[]"); +} + +#[test] +fn inactive_child_document_keeps_reachable_wrappers_without_retaining_unreachable_nodes() { + for loaded_srcdoc in [false, true] { + let mut vm = inactive_document_test_vm(loaded_srcdoc); + vm.eval( + r#" + globalThis.retainedDocument = registryFrame.contentDocument; + globalThis.retainedBody = retainedDocument.body; + globalThis.retainedChildren = retainedBody.children; + globalThis.retainedNode = retainedDocument.createElement('div'); + retainedNode.marker = {}; + globalThis.retainedMarker = retainedNode.marker; + retainedBody.appendChild(retainedNode); + globalThis.weakBeforeRemoval = new WeakRef(retainedDocument.createElement('aside')); + registryFrame.remove(); + "#, + ) + .unwrap(); + vm.drain_pending_child_frame_work_for_test(); + vm.eval( + "globalThis.weakAfterRemoval = new WeakRef(retainedDocument.createElement('section'));", + ) + .unwrap(); + vm.renderer_document_isolate + .clone() + .with_entered_renderer_document_isolate(|isolate| { + isolate.clear_kept_objects(); + isolate.low_memory_notification(); + Ok(()) + }) + .unwrap(); + assert_eq!( + vm.eval( + r#"JSON.stringify({ + document: retainedNode.ownerDocument === retainedDocument, + body: retainedDocument.body === retainedBody, + children: retainedBody.children === retainedChildren, + node: retainedChildren[0] === retainedNode, + expando: retainedChildren[0].marker === retainedMarker, + beforeCollected: weakBeforeRemoval.deref() === undefined, + afterCollected: weakAfterRemoval.deref() === undefined + })"#, + ) + .unwrap(), + r#"{"document":true,"body":true,"children":true,"node":true,"expando":true,"beforeCollected":true,"afterCollected":true}"#, + "loaded_srcdoc={loaded_srcdoc}" + ); + } +} + +#[test] +fn inactive_child_document_and_nodes_keep_registry_associations_after_teardown() { + for loaded_srcdoc in [false, true] { + let mut vm = inactive_document_test_vm(loaded_srcdoc); + vm.eval( + r#" + globalThis.retainedDocument = registryFrame.contentDocument; + globalThis.retainedRegistry = registryFrame.contentWindow.customElements; + globalThis.scopedRegistry = new CustomElementRegistry(); + globalThis.retainedElements = [ + retainedDocument.createElement('div'), + retainedDocument.createElement('div', {customElementRegistry: retainedRegistry}), + retainedDocument.createElement('div', {customElementRegistry: scopedRegistry}), + retainedDocument.createElement('div', {customElementRegistry: null}) + ]; + registryFrame.remove(); + "#, + ) + .unwrap(); + vm.drain_pending_child_frame_work_for_test(); + assert_eq!( + vm.eval( + r#"JSON.stringify([ + retainedDocument.customElementRegistry === retainedRegistry, + retainedElements[0].customElementRegistry === retainedRegistry, + retainedElements[1].customElementRegistry === retainedRegistry, + retainedElements[2].customElementRegistry === scopedRegistry, + retainedElements[3].customElementRegistry === null, + retainedDocument.createElement('span', {customElementRegistry: retainedRegistry}) + .customElementRegistry === retainedRegistry, + retainedDocument.importNode(document.createElement('span'), + {customElementRegistry: retainedRegistry}).ownerDocument === retainedDocument, + retainedDocument.createElement('div').attachShadow({mode: 'open', + customElementRegistry: retainedRegistry}).customElementRegistry === retainedRegistry + ])"#, + ) + .unwrap(), + "[true,true,true,true,true,true,true,true]", + "loaded_srcdoc={loaded_srcdoc}" + ); + } +} + +#[test] +fn inactive_child_document_can_materialize_its_intrinsic_registry_after_teardown() { + for materialized_constructor in [false, true] { + let mut vm = inactive_document_test_vm(false); + vm.eval(&format!( + "globalThis.materializedConstructor = {materialized_constructor}" + )) + .unwrap(); + vm.eval( + r#" + globalThis.retainedDocument = registryFrame.contentDocument; + globalThis.registryPrototype = materializedConstructor + ? registryFrame.contentWindow.CustomElementRegistry.prototype : null; + globalThis.registryPropertyReads = 0; + for (const name of ['customElements', 'CustomElementRegistry']) { + Object.defineProperty(registryFrame.contentWindow, name, {configurable: true, get() { + registryPropertyReads++; + throw new Error('public property must not supply the native registry'); + }}); + } + registryFrame.remove(); + "#, + ) + .unwrap(); + vm.drain_pending_child_frame_work_for_test(); + assert_eq!( + vm.eval( + r#"(() => { + const registry = retainedDocument.customElementRegistry; + globalThis.weakRegistry = new WeakRef(registry); + return JSON.stringify([ + !materializedConstructor || Object.getPrototypeOf(registry) === registryPrototype, + Object.prototype.toString.call(registry) === '[object CustomElementRegistry]', + retainedDocument.customElementRegistry === registry, + retainedDocument.createElement('div').customElementRegistry === registry, + registryPropertyReads === 0 + ]); + })()"#, + ) + .unwrap(), + "[true,true,true,true,true]", + "materialized_constructor={materialized_constructor}" + ); + vm.renderer_document_isolate + .clone() + .with_entered_renderer_document_isolate(|isolate| { + isolate.clear_kept_objects(); + isolate.low_memory_notification(); + Ok(()) + }) + .unwrap(); + assert_eq!( + vm.eval("weakRegistry.deref() !== undefined && retainedDocument.customElementRegistry === weakRegistry.deref()") + .unwrap(), + "true", + "a retained Document must keep its lazily created registry alive" + ); + } +}