fix(dom): expose live HTMLCollections on every Document

Use the native live collection queries for XML and detached HTML Documents,
with stable receiver-realm objects instead of detached snapshots.

Match HTML elements and unnamespaced href/name attributes, include area in
links, alias plugins to embeds, and keep applets permanently empty.

Cover mutation, adoption, namespace filtering, borrowed getters and receiver
validation; remove the obsolete detached collection scanners.
This commit is contained in:
ldm0
2026-09-14 17:43:46 +08:00
parent 38ef82b716
commit aabb75beb7
11 changed files with 194 additions and 256 deletions
+9 -2
View File
@@ -76,7 +76,8 @@ impl DomHost {
self.node(handle)
.and_then(Node::as_element)
.is_some_and(|element| {
element.is_html_element("a") && element.has_attribute("href")
(element.is_html_element("a") || element.is_html_element("area"))
&& element.has_attribute_ns("", "href")
})
})
}
@@ -85,10 +86,16 @@ impl DomHost {
self.node(handle)
.and_then(Node::as_element)
.is_some_and(|element| {
element.is_html_element("a") && element.has_attribute("name")
element.is_html_element("a") && element.has_attribute_ns("", "name")
})
})
}
LiveCollectionCacheKind::Embeds => {
self.collect_matching_elements(root, include_root, |handle| {
self.is_html_element_named(handle, "embed")
})
}
LiveCollectionCacheKind::Applets => Vec::new(),
LiveCollectionCacheKind::TableRows => self.table_row_elements(root),
LiveCollectionCacheKind::TableBodies => self.table_body_elements(root),
LiveCollectionCacheKind::TableSectionRows => self.table_section_row_elements(root),
+4
View File
@@ -319,6 +319,8 @@ pub(super) enum LiveCollectionCacheKind {
Scripts,
Links,
Anchors,
Embeds,
Applets,
TableRows,
TableBodies,
TableSectionRows,
@@ -343,6 +345,8 @@ impl LiveCollectionCacheKind {
"scripts" => Some(Self::Scripts),
"links" => Some(Self::Links),
"anchors" => Some(Self::Anchors),
"embeds" => Some(Self::Embeds),
"applets" => Some(Self::Applets),
"tableRows" => Some(Self::TableRows),
"tableBodies" => Some(Self::TableBodies),
"tableSectionRows" => Some(Self::TableSectionRows),
@@ -21,6 +21,8 @@ pub(in crate::native_bridge::collections) fn callback_arg_live_collection_kind(
Some("scripts") => Some(LiveCollectionQueryKind::Scripts),
Some("links") => Some(LiveCollectionQueryKind::Links),
Some("anchors") => Some(LiveCollectionQueryKind::Anchors),
Some("embeds") => Some(LiveCollectionQueryKind::Embeds),
Some("applets") => Some(LiveCollectionQueryKind::Applets),
_ => None,
}
}
+30 -83
View File
@@ -21,7 +21,7 @@ use super::node::{
};
use super::{
JsContextHost, callback_arg_namespace, callback_arg_string, collections,
identity::{CollectionKind, LiveCollectionDescriptor, LiveCollectionQueryKind},
identity::{CollectionKind, LiveCollectionQueryKind},
runtime_ptr_from_object, set_wrapped_handle_or_null, throw_dom_exception,
validate_attribute_name, validate_element_name, validate_qualified_element_name_and_namespace,
validate_qualified_name_and_namespace,
@@ -159,9 +159,6 @@ pub(in crate::native_bridge) use detached_install::{
set_detached_text_replacement_value,
};
use detached_install::{
detached_document_anchors_value, detached_document_applets_value,
detached_document_embeds_value, detached_document_forms_value, detached_document_images_value,
detached_document_links_value, detached_document_scripts_value,
install_detached_character_data_instance_properties,
install_detached_document_instance_properties,
install_detached_document_type_instance_properties,
@@ -1553,7 +1550,7 @@ fn document_forms_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
document_html_collection_getter(scope, args, rv, DocumentCollectionAccessorKind::Forms);
document_html_collection_getter(scope, args, rv, LiveCollectionQueryKind::Forms);
}
fn document_images_getter_function<'s>(
@@ -1561,7 +1558,7 @@ fn document_images_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
document_html_collection_getter(scope, args, rv, DocumentCollectionAccessorKind::Images);
document_html_collection_getter(scope, args, rv, LiveCollectionQueryKind::Images);
}
fn document_scripts_getter_function<'s>(
@@ -1569,7 +1566,7 @@ fn document_scripts_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
document_html_collection_getter(scope, args, rv, DocumentCollectionAccessorKind::Scripts);
document_html_collection_getter(scope, args, rv, LiveCollectionQueryKind::Scripts);
}
fn document_links_getter_function<'s>(
@@ -1577,7 +1574,7 @@ fn document_links_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
document_html_collection_getter(scope, args, rv, DocumentCollectionAccessorKind::Links);
document_html_collection_getter(scope, args, rv, LiveCollectionQueryKind::Links);
}
fn document_anchors_getter_function<'s>(
@@ -1585,7 +1582,7 @@ fn document_anchors_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
document_html_collection_getter(scope, args, rv, DocumentCollectionAccessorKind::Anchors);
document_html_collection_getter(scope, args, rv, LiveCollectionQueryKind::Anchors);
}
fn document_embeds_getter_function<'s>(
@@ -1593,7 +1590,7 @@ fn document_embeds_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
document_html_collection_getter(scope, args, rv, DocumentCollectionAccessorKind::Embeds);
document_html_collection_getter(scope, args, rv, LiveCollectionQueryKind::Embeds);
}
fn document_plugins_getter_function<'s>(
@@ -1601,7 +1598,7 @@ fn document_plugins_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
document_html_collection_getter(scope, args, rv, DocumentCollectionAccessorKind::Plugins);
document_html_collection_getter(scope, args, rv, LiveCollectionQueryKind::Embeds);
}
fn document_applets_getter_function<'s>(
@@ -1609,92 +1606,42 @@ fn document_applets_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
document_html_collection_getter(scope, args, rv, DocumentCollectionAccessorKind::Applets);
}
#[derive(Clone, Copy)]
enum DocumentCollectionAccessorKind {
Forms,
Images,
Scripts,
Links,
Anchors,
Embeds,
Plugins,
Applets,
document_html_collection_getter(scope, args, rv, LiveCollectionQueryKind::Applets);
}
fn document_html_collection_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
kind: DocumentCollectionAccessorKind,
kind: LiveCollectionQueryKind,
) {
let receiver = args.this();
let Some((runtime_ptr, handle)) = document_receiver_runtime_and_handle(scope, receiver) else {
rv.set_undefined();
throw_type_error(
scope,
"Document collection getter called on incompatible receiver.",
);
return;
};
let runtime = unsafe { &*runtime_ptr };
if !is_html_document(runtime, handle) {
rv.set_undefined();
return;
}
if detached_native_handle_for_runtime(scope, runtime_ptr, receiver).is_some() {
match detached_document_collection_for_kind(scope, receiver, kind) {
Some(collection) => rv.set(collection.into()),
None => rv.set_null(),
}
return;
}
let (query_kind, query, tag_name_html_document) = match kind {
DocumentCollectionAccessorKind::Forms => (LiveCollectionQueryKind::Forms, None, None),
DocumentCollectionAccessorKind::Images => (LiveCollectionQueryKind::Images, None, None),
DocumentCollectionAccessorKind::Scripts => (LiveCollectionQueryKind::Scripts, None, None),
DocumentCollectionAccessorKind::Links => (LiveCollectionQueryKind::Links, None, None),
DocumentCollectionAccessorKind::Anchors => (LiveCollectionQueryKind::Anchors, None, None),
DocumentCollectionAccessorKind::Embeds | DocumentCollectionAccessorKind::Plugins => (
LiveCollectionQueryKind::TagName,
Some("embed".to_owned()),
Some(true),
),
DocumentCollectionAccessorKind::Applets => (
LiveCollectionQueryKind::TagName,
Some("__moli-never-match__".to_owned()),
Some(true),
),
};
let descriptor = LiveCollectionDescriptor {
collection_kind: CollectionKind::HtmlCollection,
query_kind,
root: handle,
query,
include_root: true,
tag_name_html_document,
resolution_cache: Default::default(),
};
let collection = collections::build_live_collection_wrapper(scope, runtime_ptr, descriptor);
// A child Document wrapper can precede its Window realm. Follow the
// associated Window once available, including through a borrowed getter.
let context = document_associated_window_for_object(scope, runtime_ptr, handle, receiver)
.and_then(|window| window.get_creation_context(scope))
.or_else(|| receiver.get_creation_context(scope))
.expect("Document must have a creation context");
let scope = &mut v8::ContextScope::new(scope, context);
let collection = collections::build_live_collection_for_node(
scope,
runtime_ptr,
handle,
CollectionKind::HtmlCollection,
kind,
None,
false,
);
rv.set(collection.into());
}
fn detached_document_collection_for_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
kind: DocumentCollectionAccessorKind,
) -> Option<v8::Local<'s, v8::Object>> {
match kind {
DocumentCollectionAccessorKind::Forms => detached_document_forms_value(scope, document),
DocumentCollectionAccessorKind::Images => detached_document_images_value(scope, document),
DocumentCollectionAccessorKind::Scripts => detached_document_scripts_value(scope, document),
DocumentCollectionAccessorKind::Links => detached_document_links_value(scope, document),
DocumentCollectionAccessorKind::Anchors => detached_document_anchors_value(scope, document),
DocumentCollectionAccessorKind::Embeds | DocumentCollectionAccessorKind::Plugins => {
detached_document_embeds_value(scope, document)
}
DocumentCollectionAccessorKind::Applets => detached_document_applets_value(scope, document),
}
}
fn document_default_view_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -1,7 +1,5 @@
mod attributes;
mod content;
mod document_collection_scan;
mod document_collections;
mod document_tree_scan;
mod form_association;
mod iframe;
@@ -17,7 +15,6 @@ mod shadow;
mod url_helpers;
pub(in crate::native_bridge) use self::content::set_detached_text_replacement_value;
pub(in crate::native_bridge::document) use self::document_collections::*;
pub(in crate::native_bridge) use self::form_association::{
detached_form_owner_object, detached_label_control_object,
};
@@ -1,83 +0,0 @@
use crate::{
native_bridge::document::{
build_detached_html_collection, call_object_method, detached_document_element_object,
detached_element_local_name, detached_native_handle, detached_native_object_for_handle,
read_detached_native_has_attribute,
},
util::{context_host_ptr_from_global_bridge, v8_string},
};
use super::document_tree_scan::collect_detached_elements;
pub(in crate::native_bridge::document) fn detached_document_collection_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
mut matches: impl FnMut(&mut v8::PinScope<'s, '_>, v8::Local<'s, v8::Object>) -> bool,
) -> Option<v8::Local<'s, v8::Object>> {
if let Some(values) = detached_native_document_collection(scope, document, &mut matches) {
return build_detached_html_collection(scope, &values);
}
let mut values = Vec::new();
if let Some(root) = detached_document_element_object(scope, document) {
collect_detached_elements(scope, root, &mut values, &mut matches);
}
build_detached_html_collection(scope, &values)
}
fn detached_native_document_collection<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
matches: &mut impl FnMut(&mut v8::PinScope<'s, '_>, v8::Local<'s, v8::Object>) -> bool,
) -> Option<Vec<v8::Local<'s, v8::Object>>> {
let runtime_ptr = context_host_ptr_from_global_bridge(scope)?;
let root = detached_native_handle(scope, document)?;
let handles = {
let dom_host = unsafe { &*runtime_ptr }.dom_host();
let mut stack = dom_host.child_handles_reversed(root).collect::<Vec<_>>();
let mut handles = Vec::new();
while let Some(handle) = stack.pop() {
if dom_host
.node(handle)
.and_then(crate::dom::native::Node::as_element)
.is_some()
{
handles.push(handle);
}
stack.extend(dom_host.child_handles_reversed(handle));
}
handles
};
let mut out = Vec::new();
for handle in handles {
let Some(node) = detached_native_object_for_handle(scope, runtime_ptr, handle) else {
continue;
};
if matches(scope, node) {
out.push(node);
}
}
Some(out)
}
pub(super) fn detached_element_local_name_is<'s>(
scope: &mut v8::PinScope<'s, '_>,
node: v8::Local<'s, v8::Object>,
expected: &str,
) -> bool {
detached_element_local_name(scope, node).is_some_and(|name| name.eq_ignore_ascii_case(expected))
}
pub(super) fn detached_element_has_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
node: v8::Local<'s, v8::Object>,
name: &str,
) -> bool {
if let Some(has_attribute) = read_detached_native_has_attribute(scope, node, name) {
return has_attribute;
}
let Some(name) = v8_string(scope, name) else {
return false;
};
call_object_method(scope, node, "hasAttribute", &[name.into()])
.is_some_and(|value| value.boolean_value(scope))
}
@@ -1,68 +0,0 @@
use super::document_collection_scan::{
detached_document_collection_value, detached_element_has_attribute,
detached_element_local_name_is,
};
pub(in crate::native_bridge::document) fn detached_document_images_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
detached_document_collection_value(scope, document, |scope, node| {
detached_element_local_name_is(scope, node, "img")
})
}
pub(in crate::native_bridge::document) fn detached_document_embeds_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
detached_document_collection_value(scope, document, |scope, node| {
detached_element_local_name_is(scope, node, "embed")
})
}
pub(in crate::native_bridge::document) fn detached_document_links_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
detached_document_collection_value(scope, document, |scope, node| {
(detached_element_local_name_is(scope, node, "a")
|| detached_element_local_name_is(scope, node, "area"))
&& detached_element_has_attribute(scope, node, "href")
})
}
pub(in crate::native_bridge::document) fn detached_document_forms_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
detached_document_collection_value(scope, document, |scope, node| {
detached_element_local_name_is(scope, node, "form")
})
}
pub(in crate::native_bridge::document) fn detached_document_scripts_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
detached_document_collection_value(scope, document, |scope, node| {
detached_element_local_name_is(scope, node, "script")
})
}
pub(in crate::native_bridge::document) fn detached_document_anchors_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
detached_document_collection_value(scope, document, |scope, node| {
detached_element_local_name_is(scope, node, "a")
&& detached_element_has_attribute(scope, node, "name")
})
}
pub(in crate::native_bridge::document) fn detached_document_applets_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
detached_document_collection_value(scope, document, |_scope, _node| false)
}
@@ -1,20 +1,5 @@
use super::super::super::{detached_child_node_objects, detached_node_type};
pub(super) fn collect_detached_elements<'s>(
scope: &mut v8::PinScope<'s, '_>,
root: v8::Local<'s, v8::Object>,
out: &mut Vec<v8::Local<'s, v8::Object>>,
matches: &mut impl FnMut(&mut v8::PinScope<'s, '_>, v8::Local<'s, v8::Object>) -> bool,
) {
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if detached_node_type(scope, node) == Some(1) && matches(scope, node) {
out.push(node);
}
stack.extend(detached_child_node_objects(scope, node).into_iter().rev());
}
}
pub(super) fn find_detached_element<'s>(
scope: &mut v8::PinScope<'s, '_>,
root: v8::Local<'s, v8::Object>,
@@ -184,6 +184,8 @@ pub(super) enum LiveCollectionQueryKind {
Scripts,
Links,
Anchors,
Embeds,
Applets,
Labels,
TableRows,
TableBodies,
@@ -212,6 +214,8 @@ impl LiveCollectionQueryKind {
Self::Scripts => "scripts",
Self::Links => "links",
Self::Anchors => "anchors",
Self::Embeds => "embeds",
Self::Applets => "applets",
Self::Labels => "labels",
Self::TableRows => "tableRows",
Self::TableBodies => "tableBodies",
@@ -3598,7 +3598,7 @@ fn detached_document_state_and_collections_use_document_prototype_accessors() {
assert(parsed.images.length === 1, "parsed images");
assert(parsed.hidden === true, "parsed hidden");
assert(parsed.visibilityState === "hidden", "parsed visibility");
assert(xml.images === undefined, "xml images");
assert(xml.images instanceof HTMLCollection && xml.images.length === 0, "xml images");
assert(xml.hidden === true, "xml hidden");
assert(xml.visibilityState === "hidden", "xml visibility");
@@ -3627,6 +3627,104 @@ fn detached_document_state_and_collections_use_document_prototype_accessors() {
);
}
#[test]
fn document_collections_track_html_elements_in_xml_and_detached_html_documents() {
let mut vm = new_storage_test_vm("https://document-collections.test/");
let result = vm
.eval(
r#"
(() => {
const html = "http://www.w3.org/1999/xhtml";
const assert = (ok, message) => { if (!ok) throw new Error(message); };
const equal = (list, values, message) => assert(
list.length === values.length && values.every((value, i) => list[i] === value), message
);
const factories = [
() => new Document(),
() => document.implementation.createDocument(null, "root"),
() => document.implementation.createDocument("http://www.w3.org/2000/svg", "svg"),
() => new DOMParser().parseFromString("<root/>", "application/xml"),
() => new DOMParser().parseFromString(`<root xmlns="${html}"/>`, "application/xhtml+xml"),
() => document.implementation.createHTMLDocument(""),
() => new DOMParser().parseFromString("<!doctype html><body></body>", "text/html")
];
const kinds = [
["images", "img"], ["forms", "form"], ["scripts", "script"],
["links", "a", "href"], ["anchors", "a", "name"],
["embeds", "embed"], ["plugins", "embed"], ["applets", "applet"]
];
const errors = [];
for (const [index, factory] of factories.entries()) {
for (const [name, tag, attr] of kinds) {
try {
const doc = factory(), list = doc[name];
const root = doc.body || doc.documentElement || doc.appendChild(doc.createElement("root"));
assert(list instanceof HTMLCollection, "HTMLCollection brand");
assert(list === doc[name], "SameObject before mutation");
const make = (ns, qualifiedName) => {
const node = doc.createElementNS(ns, qualifiedName);
if (attr) node.setAttributeNS(null, attr, "");
root.appendChild(node);
return node;
};
make(null, tag);
make("urn:foreign", tag);
make(html, tag.toUpperCase());
const first = make(html, tag), second = make(html, "h:" + tag);
if (name === "applets") {
make(html, "object");
make(html, "__moli-never-match__");
equal(list, [], "applets never matches elements");
continue;
}
equal(list, [first, second], "exact namespace and local name");
first.setAttribute("id", "first");
second.setAttribute("name", "second");
assert(list.namedItem("first") === first && list.second === second, "named access");
root.insertBefore(second, first);
equal(list, [second, first], "reordering");
assert(list.item(1) === first, "item identity");
const other = document.implementation.createDocument(null, "other");
const otherList = other[name];
other.documentElement.appendChild(other.adoptNode(second));
equal(list, [first], "adoption removes member");
equal(otherList, [second], "adoption adds member");
first.setAttribute("id", "renamed");
assert(list.namedItem("first") === null && list.renamed === first, "renamed id");
if (attr) {
first.removeAttributeNS(null, attr);
first.setAttributeNS("urn:attribute", attr, "");
first.setAttributeNS(null, attr.toUpperCase(), "");
equal(list, [], "namespaced and uppercase attributes excluded");
first.setAttributeNS(null, attr, "");
equal(list, [first], "empty unnamespaced attribute included");
}
if (name === "links") {
const area = make(html, "area");
equal(list, [first, area], "area with href included");
area.removeAttributeNS(null, "href");
equal(list, [first], "area href removal");
}
first.remove();
equal(list, [], "node removal");
assert(list.renamed === undefined, "removed named property");
assert(list === doc[name], "SameObject after mutation");
assert(doc.plugins === doc.embeds, "plugins aliases embeds");
} catch (error) {
errors.push(`${index}/${name}: ${error.message}`);
}
}
}
return JSON.stringify(errors);
})()
"#,
)
.expect("Document collection mutation and filtering probes should evaluate");
assert_eq!(result, "[]");
}
#[test]
fn constructed_documents_share_their_associated_document_origin() {
let mut vm = new_storage_test_vm("https://www.example.com/path");
@@ -1344,7 +1344,7 @@ fn document_state_and_collection_accessors_live_on_document_prototype() {
const xml = document.implementation.createDocument("urn:test", "root", null);
assert(!own(xml, "images"), "xml images should not be own");
assert(xml.images === undefined, "xml images value");
assert(xml.images instanceof HTMLCollection && xml.images.length === 0, "xml images value");
assert(xml.hidden === true, "xml hidden value");
assert(xml.visibilityState === "hidden", "xml visibility value");
@@ -1376,6 +1376,51 @@ fn document_state_and_collection_accessors_live_on_document_prototype() {
);
}
#[test]
fn document_collection_getters_preserve_receiver_realm_and_validate_brand() {
let mut vm = new_parsed_test_vm(
"https://document-collection-realm.test/",
"<!doctype html><html><body><iframe id='child'></iframe></body></html>",
);
materialize_single_child_default_realm_for_test(&mut vm, "Document collection child realm");
let result = vm
.eval(
r#"
(() => {
const assert = (ok, message) => { if (!ok) throw new Error(message); };
const child = document.getElementById("child").contentWindow;
const doc = child.document;
for (const name of ["forms", "images", "scripts", "links", "anchors", "embeds", "plugins", "applets"]) {
const getter = Object.getOwnPropertyDescriptor(Document.prototype, name).get;
const collection = getter.call(doc);
assert(Object.getPrototypeOf(collection) === child.HTMLCollection.prototype, `${name} realm`);
assert(collection === doc[name], `${name} SameObject across realms`);
for (const receiver of [{}, document.createElement("div"), document.createDocumentFragment(), null]) {
let error;
try { getter.call(receiver); } catch (caught) { error = caught; }
assert(error instanceof TypeError, `${name} receiver brand`);
}
}
const embeds = doc.embeds;
const embed = doc.createElement("embed");
const foreign = doc.createElementNS("urn:foreign", "embed");
doc.body.appendChild(foreign);
assert(embeds.length === 0, "foreign embed excluded");
doc.body.appendChild(embed);
assert(embeds.length === 1 && embeds[0] === embed, "live child collection");
assert(embeds === doc.plugins, "child plugins aliases embeds");
embed.remove();
assert(embeds.length === 0, "child collection removal");
return "ok";
})()
"#,
)
.expect("Document collection realm and brand probes should evaluate");
assert_eq!(result, "ok");
}
#[test]
fn geometry_accessors_live_on_owner_prototypes() {
let mut vm = new_parsed_test_vm(