perf(forms): resolve named items from indexed candidates

This commit is contained in:
ldm0
2026-09-29 09:15:04 +08:00
parent b7abc1dcaa
commit 27008fc731
7 changed files with 533 additions and 132 deletions
+3
View File
@@ -584,6 +584,9 @@ impl DomHost {
let _ = clone_element.set_media_network_state(u32::MAX);
let _ = clone_element.set_media_network_state(element.media_network_state());
let _ = clone_element.set_media_error_code(element.media_error_code());
// Cloning copies attributes directly, bypassing set_attribute's index updates.
// Detached clones must be queryable before any tree insertion refreshes them.
self.record_named_index_candidate(clone);
}
}
+107 -45
View File
@@ -506,6 +506,50 @@ impl DomHost {
}
}
/// Compare nodes in the order of a traversal that visits shadow children
/// before light children. Ordinary document-position comparison treats
/// different shadow trees as disconnected and cannot order these queries.
pub fn compare_handles_in_shadow_including_tree_order(
&self,
left: DomHandle,
right: DomHandle,
) -> std::cmp::Ordering {
let ancestors = |handle| {
let mut path = Vec::new();
let mut current = Some(handle);
while let Some(handle) = current {
path.push(handle);
current = self
.parent_node(handle)
.or_else(|| self.shadow_root_host(handle));
}
path.reverse();
path
};
let left_path = ancestors(left);
let right_path = ancestors(right);
let common = left_path
.iter()
.zip(&right_path)
.take_while(|(a, b)| a == b)
.count();
if common == 0 {
return left.index().cmp(&right.index());
}
match (left_path.get(common), right_path.get(common)) {
(Some(left), Some(right)) => {
if self.is_shadow_root(*left) {
std::cmp::Ordering::Less
} else if self.is_shadow_root(*right) {
std::cmp::Ordering::Greater
} else {
self.compare_handles_in_document_order(*left, *right)
}
}
_ => left_path.len().cmp(&right_path.len()),
}
}
pub fn is_connected(&self, handle: DomHandle) -> bool {
self.node(handle)
.is_some_and(|node| node.flags().connected())
@@ -824,22 +868,60 @@ impl DomHost {
/// rather than caching misses (custom-element upgrades can change eligibility
/// without changing an element's id or name).
pub fn has_element_with_named_item_key(&self, key: &str) -> bool {
self.visit_element_candidates_by_id_or_name(key, |handle| {
if self
.node(handle)
.and_then(Node::as_element)
.is_some_and(|element| element.matches_named_item_key(key))
{
std::ops::ControlFlow::Break(())
} else {
std::ops::ControlFlow::Continue(())
}
})
.is_break()
}
/// Visit current id/name candidates once each, without restricting document,
/// connectivity, or tree scope. Callers supply their eligibility rules and
/// sort accepted nodes if needed. Breaking avoids building a result list or
/// inspecting the remaining candidates for existence-only queries.
pub fn visit_element_candidates_by_id_or_name<B>(
&self,
key: &str,
mut visit: impl FnMut(DomHandle) -> std::ops::ControlFlow<B>,
) -> std::ops::ControlFlow<B> {
if key.is_empty() {
return false;
return std::ops::ControlFlow::Continue(());
}
self.ensure_id_index();
self.ensure_name_index();
let matches = |index: &NamedElementIndex| {
index.handles_by_value.get(key).is_some_and(|handles| {
handles.iter().any(|handle| {
self.node(*handle)
.and_then(Node::as_element)
.is_some_and(|element| element.matches_named_item_key(key))
let ids = self.id_index.borrow();
let names = self.name_index.borrow();
let ids = ids
.as_ref()
.and_then(|index| index.handles_by_value.get(key));
let names = names
.as_ref()
.and_then(|index| index.handles_by_value.get(key));
let candidates = ids.into_iter().flat_map(NamedElementHandles::iter).chain(
names
.into_iter()
.flat_map(NamedElementHandles::iter)
.filter(|handle| ids.is_none_or(|ids| !ids.contains(handle))),
);
for &handle in candidates {
if self
.node(handle)
.and_then(Node::as_element)
.is_some_and(|element| {
element.id() == Some(key) || element.name_attribute() == Some(key)
})
})
};
self.id_index.borrow().as_ref().is_some_and(matches)
|| self.name_index.borrow().as_ref().is_some_and(matches)
{
visit(handle)?;
}
}
std::ops::ControlFlow::Continue(())
}
pub fn element_handles_by_id_or_name_matching(
@@ -847,41 +929,21 @@ impl DomHost {
key: &str,
mut accepts_name: impl FnMut(DomHandle) -> bool,
) -> Vec<DomHandle> {
self.ensure_id_index();
self.ensure_name_index();
let mut candidates = IndexSet::new();
if let Some(handles) = self
.id_index
.borrow()
.as_ref()
.and_then(|index| index.handles_by_value.get(key))
{
candidates.extend(handles.iter().copied());
}
if let Some(handles) = self
.name_index
.borrow()
.as_ref()
.and_then(|index| index.handles_by_value.get(key))
{
candidates.extend(handles.iter().copied());
}
let document_handle = self.document_handle();
let mut matches = candidates
.into_iter()
.filter(|handle| {
self.node(*handle).is_some_and(|node| {
node.flags().in_document_tree()
&& node.owner_document() == Some(document_handle)
&& node.as_element().is_some_and(|element| {
element.id() == Some(key)
|| (element.name_attribute() == Some(key) && accepts_name(*handle))
})
})
})
.collect::<Vec<_>>();
let mut matches = Vec::new();
let _ = self.visit_element_candidates_by_id_or_name(key, |handle| {
if self.node(handle).is_some_and(|node| {
node.flags().in_document_tree()
&& node.owner_document() == Some(document_handle)
&& node.as_element().is_some_and(|element| {
element.id() == Some(key)
|| (element.name_attribute() == Some(key) && accepts_name(handle))
})
}) {
matches.push(handle);
}
std::ops::ControlFlow::<()>::Continue(())
});
matches.sort_by(|left, right| self.compare_handles_in_document_order(*left, *right));
matches
}
+73
View File
@@ -541,6 +541,79 @@ mod tests {
assert!(!host.has_element_with_named_item_key(""));
}
#[test]
fn named_candidate_visits_are_unscoped_deduplicated_and_short_circuitable() {
use std::ops::ControlFlow;
let mut host = test_host();
assert!(!host.has_element_with_named_item_key("shared"));
let detached = host.create_element("input");
let shadow_host = host.create_element("div");
let shadow = host.attach_shadow_root(shadow_host, "open").unwrap();
let shadow_input = host.create_element("input");
assert!(host.append_child(shadow, shadow_input));
let other_document = host.create_detached_html_document();
let other_input = host.create_element("input");
assert!(host.append_child(other_document, other_input));
for handle in [detached, shadow_input, other_input] {
assert!(host.set_attribute(handle, "id", "shared"));
assert!(host.set_attribute(handle, "name", "shared"));
}
let mut found = Vec::new();
let result = host.visit_element_candidates_by_id_or_name("shared", |handle| {
found.push(handle);
ControlFlow::<()>::Continue(())
});
assert!(result.is_continue());
assert_eq!(found.len(), 3);
for handle in [detached, shadow_input, other_input] {
assert!(found.contains(&handle));
}
let mut visits = 0;
let first = host.visit_element_candidates_by_id_or_name("shared", |handle| {
visits += 1;
ControlFlow::Break(handle)
});
assert!(first.is_break());
assert_eq!(visits, 1);
assert!(host.set_attribute(detached, "id", "renamed"));
assert!(host.remove_attribute(detached, "name"));
let _ = host.visit_element_candidates_by_id_or_name("shared", |handle| {
assert_ne!(handle, detached);
ControlFlow::<()>::Continue(())
});
assert_eq!(
host.visit_element_candidates_by_id_or_name("renamed", ControlFlow::Break),
ControlFlow::Break(detached)
);
assert!(
host.element_handles_by_id_or_name_matching("shared", |_| true)
.is_empty()
);
}
#[test]
fn named_candidates_track_detached_clones_and_imports_after_materialization() {
use std::ops::ControlFlow;
let mut host = test_host();
let source = host.create_element("input");
assert!(host.set_attribute(source, "id", "cloned"));
assert!(host.set_attribute(source, "name", "cloned"));
assert!(host.has_element_with_named_item_key("cloned"));
let clone = host.clone_node(source, false).unwrap();
let document = host.create_detached_html_document();
let imported = host.import_node(document, source, false).unwrap();
let foreign = host.snapshot_document();
let foreign_import = host
.import_foreign_node(document, &foreign, source, false)
.unwrap();
let mut candidates = Vec::new();
let _ = host.visit_element_candidates_by_id_or_name("cloned", |handle| {
candidates.push(handle);
ControlFlow::<()>::Continue(())
});
assert_eq!(candidates, vec![source, clone, imported, foreign_import]);
}
#[test]
fn cached_tag_name_collection_reuses_query_until_mutation() {
let mut host = test_host();
+7
View File
@@ -81,6 +81,13 @@ pub(super) enum NamedElementHandles {
}
impl NamedElementHandles {
pub(super) fn contains(&self, handle: &DomHandle) -> bool {
match self {
Self::One(current) => current == handle,
Self::Many(handles) => handles.contains(handle),
}
}
pub(super) fn insert(&mut self, handle: DomHandle) {
match self {
Self::One(current) if *current == handle => {}
+27 -8
View File
@@ -36,28 +36,47 @@ mod focus;
mod forms;
#[cfg(test)]
std::thread_local! {
static FORM_LOOKUP_WORK: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) };
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct FormLookupWork {
pub traversals: u64,
pub enumerations: u64,
pub inspected_nodes: u64,
}
#[cfg(test)]
pub(crate) fn take_form_lookup_work_for_test() -> (u64, u64) {
FORM_LOOKUP_WORK.with(|work| work.replace((0, 0)))
std::thread_local! {
static FORM_LOOKUP_WORK: std::cell::Cell<FormLookupWork> = const { std::cell::Cell::new(FormLookupWork { traversals: 0, enumerations: 0, inspected_nodes: 0 }) };
}
#[cfg(test)]
pub(crate) fn take_form_lookup_work_for_test() -> FormLookupWork {
FORM_LOOKUP_WORK.with(|work| work.replace(FormLookupWork::default()))
}
#[cfg(test)]
fn record_form_lookup_traversal_for_test() {
FORM_LOOKUP_WORK.with(|work| {
let (traversals, enumerations) = work.get();
work.set((traversals + 1, enumerations));
let mut current = work.get();
current.traversals += 1;
work.set(current);
});
}
#[cfg(test)]
fn record_form_lookup_enumeration_for_test() {
FORM_LOOKUP_WORK.with(|work| {
let (traversals, enumerations) = work.get();
work.set((traversals, enumerations + 1));
let mut current = work.get();
current.enumerations += 1;
work.set(current);
});
}
#[cfg(test)]
fn record_form_lookup_node_for_test() {
FORM_LOOKUP_WORK.with(|work| {
let mut current = work.get();
current.inspected_nodes += 1;
work.set(current);
});
}
mod geometry;
@@ -3,6 +3,7 @@ use crate::custom_elements::is_form_associated_custom_element_handle;
use crate::native_bridge::bridge::wrapped_handle_value_for_receiver;
use crate::native_bridge::element::{html_element_getter_receiver, html_element_setter_receiver};
use moli_webapi_declare::DataPropertyDescriptorDeclaration;
use std::ops::ControlFlow;
pub(in crate::native_bridge) fn form_action_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -272,35 +273,55 @@ pub(crate) fn form_control_elements(
runtime: &JsContextHost,
form_handle: DomHandle,
) -> Vec<DomHandle> {
if runtime
.dom_host()
.is_html_element_named(form_handle, "fieldset")
{
return collect_form_control_elements_from(runtime, form_handle, false, None, false);
}
if !runtime
.dom_host()
.is_html_element_named(form_handle, "form")
{
let Some(scope) = FormControlScope::new(runtime, form_handle) else {
return Vec::new();
};
collect_form_control_elements_from(runtime, scope.root, false, scope.owner, false)
}
#[derive(Clone, Copy)]
struct FormControlScope {
root: DomHandle,
owner: Option<DomHandle>,
}
impl FormControlScope {
fn new(runtime: &JsContextHost, form: DomHandle) -> Option<Self> {
let dom = runtime.dom_host();
if dom.is_html_element_named(form, "fieldset") {
return Some(Self {
root: form,
owner: None,
});
}
if !dom.is_html_element_named(form, "form") {
return None;
}
let root = if dom.is_connected(form) {
dom.owner_document_handle(form)
.unwrap_or_else(|| dom.document_handle())
} else {
form
};
Some(Self {
root,
owner: Some(form),
})
}
if !runtime.dom_host().is_connected(form_handle) {
return collect_form_control_elements_from(
runtime,
form_handle,
false,
Some(form_handle),
false,
);
fn contains(self, runtime: &JsContextHost, candidate: DomHandle) -> bool {
let dom = runtime.dom_host();
let mut current = dom.parent_node(candidate);
while let Some(handle) = current {
if handle == self.root {
return true;
}
current = dom
.parent_node(handle)
.or_else(|| dom.shadow_root_host(handle));
}
false
}
let document_handle = runtime
.dom_host()
.owner_document_handle(form_handle)
.unwrap_or_else(|| runtime.dom_host().document_handle());
collect_form_control_elements_from(runtime, document_handle, false, Some(form_handle), false)
}
pub(crate) fn form_data_control_elements(
@@ -367,10 +388,9 @@ fn collect_form_control_elements_from(
push_shadow_including_children(runtime, root, &mut stack);
}
while let Some(handle) = stack.pop() {
if is_form_control_handle(runtime, handle, include_image_inputs)
&& form_handle
.is_none_or(|owner| form_associated_form_owner(runtime, handle) == Some(owner))
{
#[cfg(test)]
crate::native_bridge::element::record_form_lookup_node_for_test();
if form_control_matches_owner(runtime, handle, form_handle, include_image_inputs) {
out.push(handle);
}
push_shadow_including_children(runtime, handle, &mut stack);
@@ -425,6 +445,16 @@ fn is_form_control_handle(
})
}
fn form_control_matches_owner(
runtime: &JsContextHost,
handle: DomHandle,
owner: Option<DomHandle>,
include_image_inputs: bool,
) -> bool {
is_form_control_handle(runtime, handle, include_image_inputs)
&& owner.is_none_or(|owner| form_associated_form_owner(runtime, handle) == Some(owner))
}
pub(in crate::native_bridge) fn fieldset_elements_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -745,9 +775,19 @@ fn form_has_named_item_or_past_name(
form_handle: DomHandle,
key: &str,
) -> bool {
!form_named_item_matches(runtime, form_handle, key)
.1
.is_empty()
let Some(scope) = FormControlScope::new(runtime, form_handle) else {
return false;
};
runtime
.dom_host()
.visit_element_candidates_by_id_or_name(key, |candidate| {
if form_named_candidate_source(runtime, scope, candidate, key).is_some() {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
})
.is_break()
|| runtime.form_past_named_item(form_handle, key).is_some()
}
@@ -945,21 +985,27 @@ fn form_named_item_matches(
form_handle: DomHandle,
key: &str,
) -> (LiveCollectionQueryKind, Vec<DomHandle>) {
// Only a miss is conclusive. A hit still needs the existing form-owner,
// custom-element, shadow-tree and image-fallback rules below. The caller
// separately checks the past-names map even when this returns no matches.
if !runtime.dom_host().has_element_with_named_item_key(key) {
let Some(scope) = FormControlScope::new(runtime, form_handle) else {
return (LiveCollectionQueryKind::FormControlsByName, Vec::new());
}
let controls = form_named_control_matches(runtime, form_handle, key);
if controls.is_empty() {
(
LiveCollectionQueryKind::FormImagesByName,
form_named_image_matches(runtime, form_handle, key),
)
} else {
(LiveCollectionQueryKind::FormControlsByName, controls)
}
};
let mut query_kind = LiveCollectionQueryKind::FormImagesByName;
let mut matches = Vec::new();
let _ = runtime
.dom_host()
.visit_element_candidates_by_id_or_name(key, |candidate| {
if let Some(source) = form_named_candidate_source(runtime, scope, candidate, key) {
if source == LiveCollectionQueryKind::FormControlsByName && source != query_kind {
query_kind = source;
matches.clear();
}
if source == query_kind {
matches.push(candidate);
}
}
ControlFlow::<()>::Continue(())
});
sort_form_named_matches(runtime, &mut matches);
(query_kind, matches)
}
pub(in crate::native_bridge) fn form_named_control_matches(
@@ -967,16 +1013,12 @@ pub(in crate::native_bridge) fn form_named_control_matches(
form_handle: DomHandle,
key: &str,
) -> Vec<DomHandle> {
form_control_elements(runtime, form_handle)
.into_iter()
.filter(|handle| {
runtime
.dom_host()
.node(*handle)
.and_then(Node::as_element)
.is_some_and(|element| element.matches_named_item_key(key))
})
.collect()
form_named_matches_from_source(
runtime,
form_handle,
key,
LiveCollectionQueryKind::FormControlsByName,
)
}
pub(in crate::native_bridge) fn form_named_image_matches(
@@ -984,25 +1026,71 @@ pub(in crate::native_bridge) fn form_named_image_matches(
form_handle: DomHandle,
key: &str,
) -> Vec<DomHandle> {
if !runtime
.dom_host()
.is_html_element_named(form_handle, "form")
{
form_named_matches_from_source(
runtime,
form_handle,
key,
LiveCollectionQueryKind::FormImagesByName,
)
}
fn form_named_matches_from_source(
runtime: &JsContextHost,
form_handle: DomHandle,
key: &str,
source: LiveCollectionQueryKind,
) -> Vec<DomHandle> {
let Some(scope) = FormControlScope::new(runtime, form_handle) else {
return Vec::new();
}
runtime
};
let mut matches = Vec::new();
let _ = runtime
.dom_host()
.elements_by_tag_name(form_handle, "img", false)
.into_iter()
.filter(|handle| nearest_form_ancestor(runtime, *handle) == Some(form_handle))
.filter(|handle| {
runtime
.dom_host()
.node(*handle)
.and_then(Node::as_element)
.is_some_and(|element| element.matches_named_item_key(key))
})
.collect()
.visit_element_candidates_by_id_or_name(key, |candidate| {
if form_named_candidate_source(runtime, scope, candidate, key) == Some(source) {
matches.push(candidate);
}
ControlFlow::<()>::Continue(())
});
sort_form_named_matches(runtime, &mut matches);
matches
}
fn form_named_candidate_source(
runtime: &JsContextHost,
scope: FormControlScope,
candidate: DomHandle,
key: &str,
) -> Option<LiveCollectionQueryKind> {
#[cfg(test)]
crate::native_bridge::element::record_form_lookup_node_for_test();
let element = runtime
.dom_host()
.node(candidate)
.and_then(Node::as_element)?;
if !element.matches_named_item_key(key) {
return None;
}
if form_control_matches_owner(runtime, candidate, scope.owner, false)
&& scope.contains(runtime, candidate)
{
Some(LiveCollectionQueryKind::FormControlsByName)
} else if element.is_html_element("img")
&& scope.owner.is_some()
&& nearest_form_ancestor(runtime, candidate) == scope.owner
{
Some(LiveCollectionQueryKind::FormImagesByName)
} else {
None
}
}
fn sort_form_named_matches(runtime: &JsContextHost, matches: &mut [DomHandle]) {
matches.sort_by(|left, right| {
runtime
.dom_host()
.compare_handles_in_shadow_including_tree_order(*left, *right)
});
}
fn nearest_form_ancestor(runtime: &JsContextHost, handle: DomHandle) -> Option<DomHandle> {
@@ -1,5 +1,151 @@
use super::*;
#[test]
fn form_named_lookup_checks_only_index_candidates_and_existence_stops_early() {
use crate::native_bridge::element::take_form_lookup_work_for_test;
let mut vm = new_parsed_test_vm(
"https://form-index-work.test/",
"<!doctype html><body></body>",
);
vm.exec(
r#"
globalThis.indexForm = document.body.appendChild(document.createElement('form'));
globalThis.firstIndexed = indexForm.appendChild(document.createElement('input'));
firstIndexed.id = firstIndexed.name = 'indexedGroup';
for (let i = 0; i < 63; ++i) {
indexForm.appendChild(document.createElement('input')).name = 'indexedGroup';
}
globalThis.otherForm = document.body.appendChild(document.createElement('form'));
otherForm.appendChild(document.createElement('input')).name = 'otherOnly';
for (let i = 0; i < 512; ++i) {
const div = document.body.appendChild(document.createElement('div'));
div.innerHTML = '<span></span><input name="unrelated">';
}
indexForm.absentPrimeKey;
"#,
None,
)
.unwrap();
take_form_lookup_work_for_test();
assert_eq!(vm.eval("'indexedGroup' in indexForm").unwrap(), "true");
let work = take_form_lookup_work_for_test();
assert_eq!(work.inspected_nodes, 1);
assert_eq!((work.traversals, work.enumerations), (0, 0));
assert_eq!(
vm.eval("indexForm.otherOnly === undefined").unwrap(),
"true"
);
let work = take_form_lookup_work_for_test();
assert_eq!(work.inspected_nodes, 1);
assert_eq!((work.traversals, work.enumerations), (0, 0));
assert_eq!(
vm.eval("globalThis.indexedList = indexForm.indexedGroup; true")
.unwrap(),
"true"
);
let work = take_form_lookup_work_for_test();
assert_eq!(
work.inspected_nodes, 64,
"id/name duplicates must be inspected once"
);
assert_eq!((work.traversals, work.enumerations), (0, 0));
assert_eq!(vm.eval("indexedList.length").unwrap(), "64");
let work = take_form_lookup_work_for_test();
assert_eq!(
work.inspected_nodes, 64,
"live list evaluation must also use candidates"
);
assert_eq!(work.traversals, 0);
}
#[test]
fn form_named_lookup_existence_does_not_remember_past_names() {
let mut vm = new_parsed_test_vm(
"https://form-index-existence.test/",
"<!doctype html><body></body>",
);
assert_eq!(vm.eval(r#"(() => {
const form = document.body.appendChild(document.createElement('form'));
for (const tag of ['input', 'img']) {
const item = form.appendChild(document.createElement(tag));
item.name = 'existenceOnly';
if (!('existenceOnly' in form)) throw Error('missing supported name');
item.name = 'renamed';
if ('existenceOnly' in form || form.existenceOnly !== undefined) throw Error('query remembered past name');
item.remove();
}
return 'ok';
})()"#).unwrap(), "ok");
}
#[test]
fn form_named_lookup_index_survives_parsing_import_and_control_type_changes() {
let mut vm = new_parsed_test_vm(
"https://form-index-import.test/",
"<!doctype html><body></body>",
);
assert_eq!(vm.eval(r#"(() => {
const empty = document.body.appendChild(document.createElement('form'));
empty.primeIndexes;
const parsed = new DOMParser().parseFromString('<form><input name="shared"><input id="shared" name="shared"><img name="shared"><img name="shared"></form>', 'text/html');
const original = parsed.querySelector('form');
const imported = document.importNode(original, true);
for (const [mode, form] of [original, imported].entries()) {
const inputs = form.querySelectorAll('input');
const list = form.shared;
if (!list) throw Error(mode + ': missing list: ' + form.outerHTML + '; elements=' + form.elements.length);
if (list.length !== 2 || list[0] !== inputs[0] || list[1] !== inputs[1]) throw Error('parsed/imported candidates');
form.insertBefore(inputs[1], inputs[0]);
if (list[0] !== inputs[1]) throw Error('candidate insertion order leaked');
inputs[1].type = 'image';
if (list.length !== 1 || form.shared !== inputs[0]) throw Error('type eligibility');
inputs[0].type = 'image';
if (list.length !== 0 || form.shared.length !== 2 || form.shared[0].tagName !== 'IMG') throw Error('image fallback');
document.body.appendChild(document.adoptNode(form));
inputs[1].type = 'text';
if (list.length !== 1 || list[0] !== inputs[1] || form.shared !== inputs[1]) throw Error('adoption and type restoration');
}
return 'ok';
})()"#).unwrap(), "ok");
}
#[test]
fn form_named_lookup_orders_shadow_reference_controls_and_filters_fieldset_scope() {
let mut vm = new_parsed_test_vm(
"https://form-index-shadow.test/",
"<!doctype html><body></body>",
);
assert_eq!(vm.eval(r#"(() => {
const host = document.body.appendChild(document.createElement('div'));
host.id = 'formHost';
const root = host.attachShadow({mode: 'open', referenceTarget: 'form'});
root.innerHTML = '<form id="form"><input name="shared"><input name="shared"></form>';
const form = root.querySelector('form');
const inside = form.querySelectorAll('input');
const external = host.appendChild(document.createElement('input'));
external.setAttribute('form', 'formHost');
external.name = 'shared';
const before = document.body.insertBefore(document.createElement('input'), host);
before.setAttribute('form', 'formHost');
before.name = 'shared';
const list = form.shared;
function check(expected) {
if (list.length !== expected.length || expected.some((item, i) => list[i] !== item)) throw Error('shadow-including order');
}
check([before, inside[0], inside[1], external]);
document.body.appendChild(before);
check([inside[0], inside[1], external, before]);
host.remove();
check([inside[0], inside[1]]);
const fieldset = document.body.appendChild(document.createElement('fieldset'));
fieldset.innerHTML = '<input name="shared"><form><input name="shared"></form>';
const fieldsetInputs = fieldset.querySelectorAll('input');
const fieldsetList = fieldset.elements;
if (fieldsetList.length !== 2 || fieldsetList[1] !== fieldsetInputs[1] || fieldsetList.namedItem('shared') !== fieldsetInputs[0]) throw Error('fieldset candidate scope');
return 'ok';
})()"#).unwrap(), "ok");
}
#[test]
fn form_named_lookup_own_properties_take_precedence_over_controls_and_prototypes() {
let mut vm = new_parsed_test_vm(
@@ -208,18 +354,21 @@ fn form_named_lookup_misses_do_not_enumerate_or_traverse_controls() {
assert_eq!(result, "ok");
assert_eq!(
crate::native_bridge::element::take_form_lookup_work_for_test(),
(0, 0)
Default::default()
);
// Positive control: the counter must observe real supported-property work.
assert_eq!(vm.eval("lookupForms[0].present.tagName").unwrap(), "INPUT");
let (traversals, enumerations) =
crate::native_bridge::element::take_form_lookup_work_for_test();
assert!(traversals > 0);
assert_eq!(enumerations, 0);
let work = crate::native_bridge::element::take_form_lookup_work_for_test();
assert!(work.inspected_nodes > 0);
assert_eq!(work.traversals, 0);
assert_eq!(work.enumerations, 0);
vm.eval("Object.getOwnPropertyNames(lookupForms[0]).length")
.unwrap();
assert!(crate::native_bridge::element::take_form_lookup_work_for_test().1 > 0);
let work = crate::native_bridge::element::take_form_lookup_work_for_test();
assert!(work.enumerations > 0);
assert!(work.traversals > 0);
assert!(work.inspected_nodes > 0);
}
#[test]