fix(dom): preserve retained iframe document creation and identity

Keep Document registry associations after iframe removal and resolve default
registry objects through the Document realm's intrinsic cache. Preserve reachable
DOM wrappers with weak entries before retiring child execution contexts,
including prebootstrapped realms, without reviving inactive scripts.

Cover blank/srcdoc documents, cross-task registry operations, lazy creation with
replaced public properties, wrapper identity, and garbage collection.

Validation: cargo fmt --all; workspace/all-targets/all-features clippy with
-D warnings; cargo nextest run --no-fail-fast (19462 passed, 13 skipped).
All 52 browser assertions pass; 21 upstream WPT results retain the baseline
20 passes and one existing DOMParser XHTML namespace failure.
This commit is contained in:
ldm0
2026-09-23 00:14:09 +08:00
parent 4aec55df56
commit ade7fbc5a1
13 changed files with 506 additions and 77 deletions
@@ -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,
+3 -3
View File
@@ -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;
@@ -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<v8::Map>);
fn window_registry_cache_key<'s>(
scope: &mut v8::PinScope<'s, '_>,
child_handle: Option<DomHandle>,
) -> 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<DomHandle>,
) -> Result<v8::Local<'s, v8::Object>> {
let context = scope.get_current_context();
let cached = context
.get_slot::<WindowRegistryCache>()
.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::<v8::Object>::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::<WindowRegistryCache>()
.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<DomHandle>,
) -> Result<v8::Local<'s, v8::Object>> {
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<v8::Local<'s, v8::Object>> {
let ctor = global
.get(scope, v8str(scope, "CustomElementRegistry").into())
.and_then(|value| v8::Local::<v8::Object>::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::<v8::Object>::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)
@@ -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
}
@@ -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();
}
@@ -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::<HashSet<_>>();
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,
@@ -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<v8::Local<'s, v8::Value>> {
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<v8::Local<'s, v8::Object>> {
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::<v8::Object>::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::<v8::Object>::try_from(value).ok())
crate::custom_elements::custom_elements_registry_for_current_realm(
scope,
Some(handle),
)
.ok()
}
CustomElementRegistryKey::Scoped(id) => {
let registry = self
@@ -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,
+92 -17
View File
@@ -433,11 +433,16 @@ impl StaticHandleCollectionStore {
struct BridgeContextWrapperCache {
wrappers: DenseReflectorMap<BridgeCachedWrapper>,
live_collection_wrappers: HashMap<LiveCollectionDescriptor, BridgeCachedWrapper>,
retired_wrappers: HashMap<ReflectorId, v8::Weak<v8::Object>>,
retired_live_collection_wrappers: HashMap<LiveCollectionDescriptor, v8::Weak<v8::Object>>,
}
#[derive(Debug)]
struct SharedDefaultWorldWrapperCache;
#[derive(Debug)]
struct RetiredWrapperContext;
#[derive(Debug)]
struct BridgeCachedWrapper {
wrapper: v8::Global<v8::Object>,
@@ -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::<SharedDefaultWorldWrapperCache>()
.is_some()
{
return;
}
if let Some(cache) = context.get_slot::<RefCell<BridgeContextWrapperCache>>() {
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::<SharedDefaultWorldWrapperCache>()
.is_some();
let realm = context
.get_slot::<RuntimeObservableContextToken>()
.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::<RetiredWrapperContext>()
.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::<RetiredWrapperContext>()
.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(
+7 -3
View File
@@ -3652,13 +3652,14 @@ impl ScriptVm {
Some(context)
}
Some(context) => {
let context_ptr = &context.context as *const v8::Global<v8::Context>;
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<v8::Context>;
self.renderer_document_isolate
.with_entered_renderer_document_isolate(|isolate| {
let scope = pin!(v8::HandleScope::new(isolate));
@@ -3788,6 +3789,9 @@ impl ScriptVm {
.collect::<Vec<_>>()
};
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<v8::Context> = &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<v8::Context> = &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),
@@ -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
@@ -1,6 +1,7 @@
use super::*;
use crate::custom_elements::CustomElementRegistryKey;
mod inactive_document;
mod insertion;
mod parser;
mod upgrade_reentry;
@@ -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 = '<!doctype html><html><head></head><body></body></html>'")
.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"
);
}
}