mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 08:00:56 +00:00
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:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
-83
@@ -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))
|
||||
}
|
||||
-68
@@ -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)
|
||||
}
|
||||
-15
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user