diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 5cae9822f7..dee039cb7f 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -90,6 +90,7 @@ pub(crate) use forms::{ form_control_elements, form_data_control_elements, is_valid_submit_button, submit_form_with_submit_event, }; +pub(in crate::native_bridge) use forms::{form_named_control_matches, form_named_image_matches}; #[cfg(test)] pub(crate) use styles::iframe_width_attribute_viewport_width; pub(crate) use styles::{ diff --git a/moli-renderer-v8/src/native_bridge/element/forms.rs b/moli-renderer-v8/src/native_bridge/element/forms.rs index a1a0111b61..56f6bdc083 100644 --- a/moli-renderer-v8/src/native_bridge/element/forms.rs +++ b/moli-renderer-v8/src/native_bridge/element/forms.rs @@ -126,9 +126,10 @@ pub(in crate::native_bridge) use self::form_element::{ form_indexed_deleter, form_indexed_descriptor, form_indexed_enumerator, form_indexed_getter, form_indexed_query, form_indexed_setter, form_length_getter_function, form_method_getter_function, form_method_setter_function, form_name_getter_function, - form_name_setter_function, form_named_definer, form_named_deleter, form_named_descriptor, - form_named_getter, form_named_query, form_no_validate_getter_function, - form_no_validate_setter_function, form_target_getter_function, form_target_setter_function, + form_name_setter_function, form_named_control_matches, form_named_definer, form_named_deleter, + form_named_descriptor, form_named_getter, form_named_image_matches, form_named_query, + form_no_validate_getter_function, form_no_validate_setter_function, + form_target_getter_function, form_target_setter_function, }; pub(crate) use self::input::{cache_input_files_from_selected_files, input_files_for_object}; pub(in crate::native_bridge) use self::input::{ diff --git a/moli-renderer-v8/src/native_bridge/element/forms/form_element.rs b/moli-renderer-v8/src/native_bridge/element/forms/form_element.rs index c03e9a37c9..5c28ea517a 100644 --- a/moli-renderer-v8/src/native_bridge/element/forms/form_element.rs +++ b/moli-renderer-v8/src/native_bridge/element/forms/form_element.rs @@ -569,11 +569,11 @@ pub(in crate::native_bridge) fn form_named_getter<'s>( }; let scope = &mut v8::ContextScope::new(scope, context); let runtime = unsafe { &mut *runtime_ptr }; - let matches = form_named_item_matches(runtime, handle, &key); + let (query_kind, matches) = form_named_item_matches(runtime, handle, &key); if matches.len() > 1 { let descriptor = LiveCollectionDescriptor { collection_kind: CollectionKind::RadioNodeList, - query_kind: LiveCollectionQueryKind::FormControlsByName, + query_kind, root: handle, query: Some(key), include_root: false, @@ -632,11 +632,11 @@ pub(in crate::native_bridge) fn form_named_descriptor<'s>( }; let scope = &mut v8::ContextScope::new(scope, context); let runtime = unsafe { &mut *runtime_ptr }; - let matches = form_named_item_matches(runtime, handle, &key); + let (query_kind, matches) = form_named_item_matches(runtime, handle, &key); let value = if matches.len() > 1 { let descriptor = LiveCollectionDescriptor { collection_kind: CollectionKind::RadioNodeList, - query_kind: LiveCollectionQueryKind::FormControlsByName, + query_kind, root: handle, query: Some(key), include_root: false, @@ -745,7 +745,9 @@ fn form_has_named_item_or_past_name( form_handle: DomHandle, key: &str, ) -> bool { - !form_named_item_matches(runtime, form_handle, key).is_empty() + !form_named_item_matches(runtime, form_handle, key) + .1 + .is_empty() || runtime.form_past_named_item(form_handle, key).is_some() } @@ -942,14 +944,30 @@ fn form_named_item_matches( runtime: &JsContextHost, form_handle: DomHandle, key: &str, -) -> Vec { +) -> (LiveCollectionQueryKind, Vec) { // 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) { - return Vec::new(); + return (LiveCollectionQueryKind::FormControlsByName, Vec::new()); } - let controls = form_control_elements(runtime, form_handle) + 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) + } +} + +pub(in crate::native_bridge) fn form_named_control_matches( + runtime: &JsContextHost, + form_handle: DomHandle, + key: &str, +) -> Vec { + form_control_elements(runtime, form_handle) .into_iter() .filter(|handle| { runtime @@ -958,15 +976,10 @@ fn form_named_item_matches( .and_then(Node::as_element) .is_some_and(|element| element.matches_named_item_key(key)) }) - .collect::>(); - if controls.is_empty() { - form_named_image_matches(runtime, form_handle, key) - } else { - controls - } + .collect() } -fn form_named_image_matches( +pub(in crate::native_bridge) fn form_named_image_matches( runtime: &JsContextHost, form_handle: DomHandle, key: &str, diff --git a/moli-renderer-v8/src/native_bridge/identity.rs b/moli-renderer-v8/src/native_bridge/identity.rs index 95fe6daa5c..95c37c70c8 100644 --- a/moli-renderer-v8/src/native_bridge/identity.rs +++ b/moli-renderer-v8/src/native_bridge/identity.rs @@ -9,7 +9,10 @@ use std::{ use indexmap::IndexSet; use super::super::document_runtime::DomHandle; -use super::element::{control_label_handles, form_control_elements}; +use super::element::{ + control_label_handles, form_control_elements, form_named_control_matches, + form_named_image_matches, +}; use super::{JsContextHost, RuntimeObservableContextToken}; use dense_reflector_map::DenseReflectorMap; @@ -175,6 +178,7 @@ pub(super) enum LiveCollectionQueryKind { WindowNamedItems, DocumentAllNamedItems, FormControlsByName, + FormImagesByName, Forms, Images, Scripts, @@ -204,6 +208,7 @@ impl LiveCollectionQueryKind { Self::WindowNamedItems => "windowNamedItems", Self::DocumentAllNamedItems => "documentAllNamedItems", Self::FormControlsByName => "formControlsByName", + Self::FormImagesByName => "formImagesByName", Self::Forms => "forms", Self::Images => "images", Self::Scripts => "scripts", @@ -302,16 +307,11 @@ impl LiveCollectionDescriptor { } else if self.query_kind == LiveCollectionQueryKind::FormControls { form_control_elements(host, self.root) } else if self.query_kind == LiveCollectionQueryKind::FormControlsByName { - let query = self.query.as_deref().unwrap_or_default(); - form_control_elements(host, self.root) - .into_iter() - .filter(|handle| { - host.dom_host() - .node(*handle) - .and_then(crate::dom::native::Node::as_element) - .is_some_and(|element| element.matches_named_item_key(query)) - }) - .collect() + form_named_control_matches(host, self.root, self.query.as_deref().unwrap_or_default()) + } else if self.query_kind == LiveCollectionQueryKind::FormImagesByName { + // A retained image list keeps this filter even if a later form[name] + // lookup finds controls. Query kind also distinguishes wrapper caches. + form_named_image_matches(host, self.root, self.query.as_deref().unwrap_or_default()) } else if self.query_kind == LiveCollectionQueryKind::WindowNamedItems { crate::native_bridge::named_access::window_named_item_handles( host.dom_host(), diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/named_lookup.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/named_lookup.rs index b19e21c6e9..3c4d1d4a2b 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/named_lookup.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/named_lookup.rs @@ -101,6 +101,73 @@ fn form_named_lookup_define_property_uses_supported_names_not_visibility() { assert_eq!(result, "ok"); } +#[test] +fn form_named_lookup_live_lists_preserve_their_control_or_image_source() { + let mut vm = new_parsed_test_vm( + "https://form-lookup-image-lists.test/", + "
", + ); + let result = vm.eval(r#" + (() => { + const detachedDocument = document.implementation.createHTMLDocument('forms'); + const forms = [ + document.getElementById('parsed'), + document.createElement('form'), + detachedDocument.body.appendChild(detachedDocument.createElement('form')) + ]; + for (const [mode, form] of forms.entries()) { + const doc = form.ownerDocument; + const first = form.appendChild(doc.createElement('img')); + first.name = 'photos'; + first.id = 'photos'; + const second = form.appendChild(doc.createElement('img')); + second.id = 'photos'; + const nested = form.appendChild(doc.createElement('form')); + nested.appendChild(doc.createElement('img')).name = 'photos'; + const imageInput = form.appendChild(doc.createElement('input')); + imageInput.type = 'image'; + imageInput.name = 'photos'; + const images = form.photos; + const descriptor = Object.getOwnPropertyDescriptor(form, 'photos'); + const descriptorImages = descriptor.value; + if (!(images instanceof RadioNodeList) || images.length !== 2 || images[0] !== first || images[1] !== second) + throw Error(mode + ': image list membership'); + if (descriptor.writable || descriptor.enumerable || !descriptor.configurable || descriptorImages.length !== 2) + throw Error(mode + ': image descriptor'); + if (form.elements.namedItem('photos') !== null) throw Error(mode + ': elements must exclude images'); + const control = form.appendChild(doc.createElement('input')); + control.name = 'photos'; + if (form.photos !== control || images.length !== 2 || images[0] !== first) + throw Error(mode + ': existing image list changed source'); + const otherControl = form.appendChild(doc.createElement('input')); + otherControl.name = 'photos'; + const controls = form.photos; + if (controls.length !== 2 || controls[0] !== control || controls[1] !== otherControl) + throw Error(mode + ': control list membership'); + control.remove(); + otherControl.remove(); + if (controls.length !== 0 || form.photos.length !== 2) throw Error(mode + ': control list fell back to images'); + form.insertBefore(second, first); + if (images[0] !== second || descriptorImages[0] !== second) throw Error(mode + ': image tree order'); + second.id = 'renamed'; + if (images.length !== 1 || images[0] !== first || descriptorImages.length !== 1 || form.photos !== first) + throw Error(mode + ': live image rename'); + first.remove(); + if (images.length !== 0 || descriptorImages.length !== 0) throw Error(mode + ': live image removal'); + second.name = 'photos'; + if (images.length !== 1 || images[0] !== second) throw Error(mode + ': live image insertion'); + if (mode === 2) { + document.body.appendChild(document.adoptNode(form)); + if (images.length !== 1 || images[0] !== second || form.photos !== second) + throw Error('adoption changed image list identity'); + } + } + return 'ok'; + })() + "#).expect("live RadioNodeLists must retain the source selected by the initial named lookup"); + assert_eq!(result, "ok"); +} + #[test] fn form_named_lookup_misses_do_not_enumerate_or_traverse_controls() { let mut vm = new_parsed_test_vm(