diff --git a/moli-dom/src/native/host/collections/live.rs b/moli-dom/src/native/host/collections/live.rs index 8974229940..dd96adbc1e 100644 --- a/moli-dom/src/native/host/collections/live.rs +++ b/moli-dom/src/native/host/collections/live.rs @@ -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), diff --git a/moli-dom/src/native/host/types.rs b/moli-dom/src/native/host/types.rs index 6588315389..da7d2f1bbe 100644 --- a/moli-dom/src/native/host/types.rs +++ b/moli-dom/src/native/host/types.rs @@ -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), diff --git a/moli-renderer-v8/src/native_bridge/collections/shared.rs b/moli-renderer-v8/src/native_bridge/collections/shared.rs index b52008a611..703c1bb499 100644 --- a/moli-renderer-v8/src/native_bridge/collections/shared.rs +++ b/moli-renderer-v8/src/native_bridge/collections/shared.rs @@ -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, } } diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index da1812836c..603df447e7 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -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> { - 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>, diff --git a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors.rs b/moli-renderer-v8/src/native_bridge/document/detached_install/accessors.rs index cc733de2d1..4c6abf110d 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_install/accessors.rs @@ -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, }; diff --git a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_collection_scan.rs b/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_collection_scan.rs deleted file mode 100644 index 7fab273524..0000000000 --- a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_collection_scan.rs +++ /dev/null @@ -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> { - 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>> { - 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::>(); - 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)) -} diff --git a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_collections.rs b/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_collections.rs deleted file mode 100644 index f1a5f666fc..0000000000 --- a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_collections.rs +++ /dev/null @@ -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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - detached_document_collection_value(scope, document, |_scope, _node| false) -} diff --git a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_tree_scan.rs b/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_tree_scan.rs index f698019723..84a23a149b 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_tree_scan.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/document_tree_scan.rs @@ -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>, - 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>, diff --git a/moli-renderer-v8/src/native_bridge/identity.rs b/moli-renderer-v8/src/native_bridge/identity.rs index c98b7972f3..8937419fd2 100644 --- a/moli-renderer-v8/src/native_bridge/identity.rs +++ b/moli-renderer-v8/src/native_bridge/identity.rs @@ -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", diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs index 2254930b58..1b6dfe2a32 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs @@ -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("", "application/xml"), + () => new DOMParser().parseFromString(``, "application/xhtml+xml"), + () => document.implementation.createHTMLDocument(""), + () => new DOMParser().parseFromString("", "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"); diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs index f898364fd6..0e41cadd16 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs @@ -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/", + "", + ); + 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(