From ef2ad11cbd9296ffdb9c68f9f62126e9ffaae329 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 16 Jul 2026 02:10:52 +0800 Subject: [PATCH] fix(forms): implement option nearest select algorithm --- .../wpt-cross-current/failed-cases.txt | 2 - .../wpt-cross-current/passed-cases.txt | 2 + moli-dom/src/forms/mod.rs | 2 + moli-dom/src/forms/option.rs | 37 ++++++++ moli-dom/src/native/host/collections/form.rs | 52 ++--------- .../src/native/host/mutation/attributes.rs | 2 +- moli-dom/src/native/host/mutation/tree.rs | 8 +- moli-dom/src/native/mod.rs | 59 +++++++++++++ moli-dom/src/native/queries.rs | 74 +++++++--------- .../tree/form_control_state.rs | 20 ++--- .../element/activation/default_action.rs | 21 +---- .../element/forms/select/helpers.rs | 19 ++-- .../element/forms/select/option.rs | 88 ++++++------------- .../src/script_vm/tests/dom_xhr/forms.rs | 74 ++++++++++++++++ 14 files changed, 259 insertions(+), 201 deletions(-) create mode 100644 moli-dom/src/forms/option.rs diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 756ec50967..13ba4aa664 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -3139,8 +3139,6 @@ html/semantics/forms/textfieldselection/selection.html html/semantics/forms/the-input-element/show-picker-cross-origin-iframe.html html/semantics/forms/the-select-element/customizable-select/base-appearance-inheritance.html html/semantics/forms/the-select-element/customizable-select/option-disabled-optgroup.html -html/semantics/forms/the-select-element/customizable-select/option-form-ancestor-select.html -html/semantics/forms/the-select-element/customizable-select/option-list.html html/semantics/forms/the-select-element/customizable-select/picker-icon-animation.html html/semantics/forms/the-select-element/customizable-select/select-base-appearance-computed-style.html html/semantics/forms/the-select-element/customizable-select/select-value-selectedOption.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index ed29dae93f..fddb048ee2 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -5847,6 +5847,8 @@ html/semantics/forms/the-select-element/common-HTMLOptionsCollection-namedItem.h html/semantics/forms/the-select-element/common-HTMLOptionsCollection.html html/semantics/forms/the-select-element/customizable-select/option-color-inheritance.html html/semantics/forms/the-select-element/customizable-select/option-disabled-invalid-nesting.html +html/semantics/forms/the-select-element/customizable-select/option-form-ancestor-select.html +html/semantics/forms/the-select-element/customizable-select/option-list.html html/semantics/forms/the-select-element/customizable-select/select-options-id.html html/semantics/forms/the-select-element/customizable-select/select-parsing.html html/semantics/forms/the-select-element/inserted-or-removed.html diff --git a/moli-dom/src/forms/mod.rs b/moli-dom/src/forms/mod.rs index ef97116c25..d2f027344e 100644 --- a/moli-dom/src/forms/mod.rs +++ b/moli-dom/src/forms/mod.rs @@ -1,6 +1,7 @@ mod button_type; mod input_type; mod numeric; +mod option; mod text; mod validity; @@ -33,6 +34,7 @@ pub use numeric::{ parse_html_floating_point_prefix, parse_input_numeric_value, progress_element_values, step_input_value, }; +pub use option::{OptionNearestSelectStep, OptionNearestSelectTraversal}; pub use text::{ normalize_custom_validation_message, normalize_form_submission_newlines, parse_non_negative_integer_prefix, parse_non_negative_length_attribute, diff --git a/moli-dom/src/forms/option.rs b/moli-dom/src/forms/option.rs new file mode 100644 index 0000000000..e626d490bd --- /dev/null +++ b/moli-dom/src/forms/option.rs @@ -0,0 +1,37 @@ +const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OptionNearestSelectStep { + Continue, + Select, + Blocked, +} + +/// State for the HTML "option element nearest ancestor select" algorithm. +/// +/// Tree owners perform the actual ancestor walk and feed element names into +/// this state machine. Keeping the HTML association rule here lets native DOM +/// trees and detached bridge trees share the same barriers. +#[derive(Debug, Default)] +pub struct OptionNearestSelectTraversal { + saw_optgroup: bool, +} + +impl OptionNearestSelectTraversal { + pub fn visit_ancestor(&mut self, namespace: &str, local_name: &str) -> OptionNearestSelectStep { + if namespace != HTML_NAMESPACE { + return OptionNearestSelectStep::Continue; + } + + match local_name { + "datalist" | "hr" | "option" => OptionNearestSelectStep::Blocked, + "optgroup" if self.saw_optgroup => OptionNearestSelectStep::Blocked, + "optgroup" => { + self.saw_optgroup = true; + OptionNearestSelectStep::Continue + } + "select" => OptionNearestSelectStep::Select, + _ => OptionNearestSelectStep::Continue, + } + } +} diff --git a/moli-dom/src/native/host/collections/form.rs b/moli-dom/src/native/host/collections/form.rs index 76afe51297..10feaa45c2 100644 --- a/moli-dom/src/native/host/collections/form.rs +++ b/moli-dom/src/native/host/collections/form.rs @@ -162,6 +162,10 @@ impl DomHost { None } + pub fn option_nearest_ancestor_select(&self, handle: DomHandle) -> Option { + self.dom.option_nearest_ancestor_select(handle) + } + pub fn radio_group_members(&self, handle: DomHandle) -> Vec { let Some(element) = self.node(handle).and_then(Node::as_element) else { return Vec::new(); @@ -188,32 +192,8 @@ impl DomHost { }) } - pub fn owner_select_for_option(&self, handle: DomHandle) -> Option { - if !self.is_html_element_named(handle, "option") { - return None; - } - let mut current = self.parent_node(handle); - while let Some(parent) = current { - let Some(element) = self.node(parent).and_then(Node::as_element) else { - current = self.parent_node(parent); - continue; - }; - if element.is_html_select() { - return Some(parent); - } - current = self.parent_node(parent); - } - None - } - pub fn select_option_elements(&self, select_handle: DomHandle) -> Vec { - if !self.is_html_element_named(select_handle, "select") { - return Vec::new(); - } - self.collect_matching_elements(select_handle, false, |handle| { - self.is_html_element_named(handle, "option") - && self.option_belongs_to_select(handle, select_handle) - }) + self.dom.select_option_elements(select_handle) } pub fn select_selected_option_elements(&self, select_handle: DomHandle) -> Vec { @@ -251,28 +231,6 @@ impl DomHost { .collect() } - fn option_belongs_to_select(&self, option: DomHandle, select_handle: DomHandle) -> bool { - let mut current = self.parent_node(option); - let mut seen_optgroup = false; - while let Some(parent) = current { - if parent == select_handle { - return true; - } - let Some(element) = self.node(parent).and_then(Node::as_element) else { - current = self.parent_node(parent); - continue; - }; - match element.local_name() { - "option" | "hr" | "select" => return false, - "optgroup" if seen_optgroup => return false, - "optgroup" => seen_optgroup = true, - _ => {} - } - current = self.parent_node(parent); - } - false - } - fn option_is_disabled(&self, handle: DomHandle) -> bool { let mut current = Some(handle); while let Some(candidate) = current { diff --git a/moli-dom/src/native/host/mutation/attributes.rs b/moli-dom/src/native/host/mutation/attributes.rs index 2070ec44f5..52e03989e3 100644 --- a/moli-dom/src/native/host/mutation/attributes.rs +++ b/moli-dom/src/native/host/mutation/attributes.rs @@ -254,7 +254,7 @@ impl DomHost { if !name.eq_ignore_ascii_case("selected") || !self.is_html_element_named(handle, "option") { return; } - let Some(select) = self.owner_select_for_option(handle) else { + let Some(select) = self.option_nearest_ancestor_select(handle) else { return; }; if self diff --git a/moli-dom/src/native/host/mutation/tree.rs b/moli-dom/src/native/host/mutation/tree.rs index b9fcb6bdf3..a55ffc22b3 100644 --- a/moli-dom/src/native/host/mutation/tree.rs +++ b/moli-dom/src/native/host/mutation/tree.rs @@ -520,7 +520,11 @@ impl DomHost { .node(option) .and_then(Node::as_element) .is_some_and(Element::selected); - (option, self.owner_select_for_option(option), selected) + ( + option, + self.option_nearest_ancestor_select(option), + selected, + ) }) .collect() } @@ -530,7 +534,7 @@ impl DomHost { snapshots: &[(DomHandle, Option, bool)], ) { for &(option, previous_select, was_selected) in snapshots { - let Some(select) = self.owner_select_for_option(option) else { + let Some(select) = self.option_nearest_ancestor_select(option) else { continue; }; if previous_select == Some(select) diff --git a/moli-dom/src/native/mod.rs b/moli-dom/src/native/mod.rs index 905404b9fa..28420bba0d 100644 --- a/moli-dom/src/native/mod.rs +++ b/moli-dom/src/native/mod.rs @@ -1529,6 +1529,65 @@ mod tests { ); } + #[test] + fn select_option_list_uses_option_nearest_ancestor_select() { + let mut host = DomHost::from_dom(NativeDom::new_html(test_url())); + let document = host.document_node_id(); + let parent_select = host.create_element("select"); + let child_select = host.create_element("select"); + assert!(host.append_child(document, parent_select)); + assert!(host.append_child(parent_select, child_select)); + + let normal_option = host.create_element("option"); + let nested_option = host.create_element("option"); + assert!(host.append_child(child_select, normal_option)); + assert!(host.append_child(normal_option, nested_option)); + + let div = host.create_element("div"); + let div_option = host.create_element("option"); + assert!(host.append_child(child_select, div)); + assert!(host.append_child(div, div_option)); + + let hr = host.create_element("hr"); + let hr_option = host.create_element("option"); + assert!(host.append_child(child_select, hr)); + assert!(host.append_child(hr, hr_option)); + + let datalist = host.create_element("datalist"); + let datalist_option = host.create_element("option"); + assert!(host.append_child(child_select, datalist)); + assert!(host.append_child(datalist, datalist_option)); + + let optgroup = host.create_element("optgroup"); + let optgroup_option = host.create_element("option"); + let nested_optgroup = host.create_element("optgroup"); + let nested_optgroup_option = host.create_element("option"); + assert!(host.append_child(child_select, optgroup)); + assert!(host.append_child(optgroup, optgroup_option)); + assert!(host.append_child(optgroup, nested_optgroup)); + assert!(host.append_child(nested_optgroup, nested_optgroup_option)); + + assert!(host.select_option_elements(parent_select).is_empty()); + assert_eq!( + host.select_option_elements(child_select), + vec![normal_option, div_option, optgroup_option] + ); + for option in [normal_option, div_option, optgroup_option] { + assert_eq!( + host.option_nearest_ancestor_select(option), + Some(child_select) + ); + } + for option in [ + nested_option, + hr_option, + datalist_option, + nested_optgroup_option, + ] { + assert_eq!(host.option_nearest_ancestor_select(option), None); + } + } + #[test] fn selected_option_insertion_deselects_single_select_peers_without_dirtying_them() { let mut host = DomHost::from_dom(NativeDom::new_html(test_url())); diff --git a/moli-dom/src/native/queries.rs b/moli-dom/src/native/queries.rs index 84853716d9..49dc9f0b7c 100644 --- a/moli-dom/src/native/queries.rs +++ b/moli-dom/src/native/queries.rs @@ -1,7 +1,9 @@ use super::NativeDom; use super::element::Element; use super::node::{NativeNodeId, Node}; -use crate::forms::parse_non_negative_integer_prefix; +use crate::forms::{ + OptionNearestSelectStep, OptionNearestSelectTraversal, parse_non_negative_integer_prefix, +}; impl NativeDom { pub fn is_html_element_named(&self, node_id: NativeNodeId, local_name: &str) -> bool { @@ -40,7 +42,7 @@ impl NativeDom { if !option.is_html_option() { return false; } - if let Some(select_id) = self.owner_select_for_option(option_id) { + if let Some(select_id) = self.option_nearest_ancestor_select(option_id) { return self .select_selected_option_elements(select_id) .contains(&option_id); @@ -58,10 +60,36 @@ impl NativeDom { } self.elements_by_tag_name(select_id, "option", false) .into_iter() - .filter(|option_id| self.option_belongs_to_select(*option_id, select_id)) + .filter(|option_id| self.option_nearest_ancestor_select(*option_id) == Some(select_id)) .collect() } + pub fn option_nearest_ancestor_select(&self, option_id: NativeNodeId) -> Option { + if !self + .node(option_id) + .and_then(Node::as_element) + .is_some_and(Element::is_html_option) + { + return None; + } + + let mut traversal = OptionNearestSelectTraversal::default(); + let mut current = self.parent_node(option_id); + while let Some(parent) = current { + let Some(element) = self.node(parent).and_then(Node::as_element) else { + current = self.parent_node(parent); + continue; + }; + match traversal.visit_ancestor(element.namespace(), element.local_name()) { + OptionNearestSelectStep::Continue => {} + OptionNearestSelectStep::Select => return Some(parent), + OptionNearestSelectStep::Blocked => return None, + } + current = self.parent_node(parent); + } + None + } + pub fn select_selected_option_elements(&self, select_id: NativeNodeId) -> Vec { let options = self.select_option_elements(select_id); let Some(select) = self.node(select_id).and_then(Node::as_element) else { @@ -181,46 +209,6 @@ impl NativeDom { out } - fn option_belongs_to_select(&self, option_id: NativeNodeId, select_id: NativeNodeId) -> bool { - let mut current = self.parent_node(option_id); - let mut seen_optgroup = false; - while let Some(parent) = current { - if parent == select_id { - return true; - } - let Some(element) = self.node(parent).and_then(Node::as_element) else { - current = self.parent_node(parent); - continue; - }; - match element.local_name() { - "option" | "hr" | "select" => return false, - "optgroup" if seen_optgroup => return false, - "optgroup" => seen_optgroup = true, - _ => {} - } - current = self.parent_node(parent); - } - false - } - - fn owner_select_for_option(&self, option_id: NativeNodeId) -> Option { - let mut current = self.parent_node(option_id); - while let Some(parent) = current { - let Some(element) = self.node(parent).and_then(Node::as_element) else { - current = self.parent_node(parent); - continue; - }; - if element.is_html_select() { - return Some(parent); - } - if matches!(element.local_name(), "option" | "hr" | "select") { - return None; - } - current = self.parent_node(parent); - } - None - } - fn option_is_disabled(&self, option_id: NativeNodeId) -> bool { let mut current = Some(option_id); while let Some(candidate) = current { diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/form_control_state.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/form_control_state.rs index 42adab00fd..fa44aa35f2 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/form_control_state.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/form_control_state.rs @@ -28,7 +28,7 @@ impl DocumentRuntime { if !element.is_html_option() { return false; } - if let Some(select) = self.owner_select_for_option(handle) { + if let Some(select) = self.dom_host.option_nearest_ancestor_select(handle) { return self .dom_host .select_selected_option_elements(select) @@ -37,17 +37,6 @@ impl DocumentRuntime { element.selected() } - fn owner_select_for_option(&self, handle: DomHandle) -> Option { - let mut current = self.dom_host.parent_node(handle); - while let Some(parent) = current { - if self.dom_host.is_html_element_named(parent, "select") { - return Some(parent); - } - current = self.dom_host.parent_node(parent); - } - None - } - fn preserve_selectedness_for_options_removed_from_select( &mut self, scope: &mut v8::PinScope<'_, '_>, @@ -55,7 +44,12 @@ impl DocumentRuntime { selectedness: &[(DomHandle, bool)], ) { for &(option, was_selected) in selectedness { - if was_selected && self.owner_select_for_option(option).is_none() { + if was_selected + && self + .dom_host + .option_nearest_ancestor_select(option) + .is_none() + { let _ = self.set_selected_state_with_dirty(scope, host_ptr, option, true, false); } } diff --git a/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs b/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs index a25396269e..f615457cc3 100644 --- a/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs +++ b/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs @@ -1464,7 +1464,7 @@ fn perform_option_click_default_action( handle: DomHandle, ) -> bool { let runtime = unsafe { &*runtime_ptr }; - let Some(select_handle) = owner_select_for_option(runtime, handle) else { + let Some(select_handle) = runtime.dom_host().option_nearest_ancestor_select(handle) else { return false; }; if is_disabled_form_control(runtime, handle) || is_disabled_form_control(runtime, select_handle) @@ -1499,25 +1499,6 @@ fn perform_option_click_default_action( true } -fn owner_select_for_option(runtime: &JsContextHost, handle: DomHandle) -> Option { - if !is_html_option_element(runtime, handle) { - return None; - } - let mut current = runtime.dom_host().parent_node(handle); - while let Some(parent) = current { - if runtime - .dom_host() - .node(parent) - .and_then(Node::as_element) - .is_some_and(Element::is_html_select) - { - return Some(parent); - } - current = runtime.dom_host().parent_node(parent); - } - None -} - fn is_html_option_element(runtime: &JsContextHost, handle: DomHandle) -> bool { runtime .dom_host() diff --git a/moli-renderer-v8/src/native_bridge/element/forms/select/helpers.rs b/moli-renderer-v8/src/native_bridge/element/forms/select/helpers.rs index 412c05b1e9..b1909a8920 100644 --- a/moli-renderer-v8/src/native_bridge/element/forms/select/helpers.rs +++ b/moli-renderer-v8/src/native_bridge/element/forms/select/helpers.rs @@ -25,20 +25,11 @@ pub(super) fn effective_option_selected(runtime: &JsContextHost, handle: DomHand return false; } - let mut current = runtime.dom_host().parent_node(handle); - while let Some(parent) = current { - let Some(parent_element) = runtime.dom_host().node(parent).and_then(Node::as_element) - else { - current = runtime.dom_host().parent_node(parent); - continue; - }; - if parent_element.is_html_select() { - return runtime - .dom_host() - .select_selected_option_elements(parent) - .contains(&handle); - } - current = runtime.dom_host().parent_node(parent); + if let Some(select) = runtime.dom_host().option_nearest_ancestor_select(handle) { + return runtime + .dom_host() + .select_selected_option_elements(select) + .contains(&handle); } element.selected() } diff --git a/moli-renderer-v8/src/native_bridge/element/forms/select/option.rs b/moli-renderer-v8/src/native_bridge/element/forms/select/option.rs index 75bd2117f6..df2dfef994 100644 --- a/moli-renderer-v8/src/native_bridge/element/forms/select/option.rs +++ b/moli-renderer-v8/src/native_bridge/element/forms/select/option.rs @@ -2,10 +2,11 @@ use super::helpers::{ effective_option_selected, element_option_value, select_is_multiple, select_option_handles, }; use super::*; +use crate::dom::forms::{OptionNearestSelectStep, OptionNearestSelectTraversal}; use crate::native_bridge::{ document::{ - detached_element_local_name, detached_form_owner_object, detached_parent_node_object, - set_detached_text_replacement_value, + detached_element_local_name, detached_element_namespace_uri, detached_form_owner_object, + detached_parent_node_object, set_detached_text_replacement_value, }, element::{html_element_getter_receiver, html_element_setter_receiver}, }; @@ -86,11 +87,19 @@ fn detached_option_form_owner_object<'s>( option: v8::Local<'s, v8::Object>, ) -> Option>> { let mut current = detached_parent_node_object(scope, option)?; + let mut traversal = OptionNearestSelectTraversal::default(); loop { - if detached_element_local_name(scope, current) - .is_some_and(|name| name.eq_ignore_ascii_case("select")) - { - return Some(detached_form_owner_object(scope, current)); + if let (Some(namespace), Some(local_name)) = ( + detached_element_namespace_uri(scope, current), + detached_element_local_name(scope, current), + ) { + match traversal.visit_ancestor(&namespace, &local_name) { + OptionNearestSelectStep::Continue => {} + OptionNearestSelectStep::Select => { + return Some(detached_form_owner_object(scope, current)); + } + OptionNearestSelectStep::Blocked => return Some(None), + } } let Some(parent) = detached_parent_node_object(scope, current) else { return Some(None); @@ -203,19 +212,7 @@ pub(in crate::native_bridge) fn option_selected_setter_function<'s>( }; let selected = args.get(0).boolean_value(scope); let runtime = unsafe { &mut *runtime_ptr }; - let mut owner_select = runtime.dom_host().parent_node(handle); - while let Some(parent) = owner_select { - if runtime - .dom_host() - .node(parent) - .and_then(Node::as_element) - .is_some_and(Element::is_html_select) - { - owner_select = Some(parent); - break; - } - owner_select = runtime.dom_host().parent_node(parent); - } + let owner_select = runtime.dom_host().option_nearest_ancestor_select(handle); if let Some(select_handle) = owner_select && selected && !select_is_multiple(runtime, select_handle) @@ -252,21 +249,11 @@ pub(in crate::native_bridge) fn option_form_getter_function<'s>( return; } let runtime = unsafe { &*runtime_ptr }; - let mut current = runtime.dom_host().parent_node(handle); - while let Some(parent) = current { - if runtime - .dom_host() - .node(parent) - .and_then(Node::as_element) - .is_some_and(Element::is_html_select) - { - let form = form_associated_form_owner(runtime, parent); - set_wrapped_node_or_null(scope, &mut rv, runtime_ptr, form); - return; - } - current = runtime.dom_host().parent_node(parent); - } - rv.set_null(); + let form = runtime + .dom_host() + .option_nearest_ancestor_select(handle) + .and_then(|select| form_associated_form_owner(runtime, select)); + set_wrapped_node_or_null(scope, &mut rv, runtime_ptr, form); } pub(in crate::native_bridge) fn option_index_getter_function<'s>( @@ -279,33 +266,16 @@ pub(in crate::native_bridge) fn option_index_getter_function<'s>( return; }; let runtime = unsafe { &*runtime_ptr }; - let mut current = runtime.dom_host().parent_node(handle); - while let Some(parent) = current { - if runtime - .dom_host() - .node(parent) - .and_then(Node::as_element) - .is_some_and(Element::is_html_select) - { - let index = select_option_handles(runtime, parent) + let index = runtime + .dom_host() + .option_nearest_ancestor_select(handle) + .and_then(|select| { + select_option_handles(runtime, select) .into_iter() .position(|option| option == handle) - .unwrap_or(0) as i32; - rv.set_int32(index); - return; - } - if runtime - .dom_host() - .node(parent) - .and_then(Node::as_element) - .is_some_and(|element| element.local_name() == "datalist") - { - rv.set_int32(0); - return; - } - current = runtime.dom_host().parent_node(parent); - } - rv.set_int32(0); + }) + .unwrap_or(0) as i32; + rv.set_int32(index); } pub(in crate::native_bridge) fn option_label_getter_function<'s>( diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs index f68dddb43a..c07c379731 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs @@ -563,6 +563,80 @@ fn detached_select_and_option_track_selection_state() { ); } +#[test] +fn option_nearest_ancestor_select_controls_list_form_index_and_selectedness() { + let mut vm = new_storage_test_vm("https://option-nearest-select.test/"); + + let result = vm + .eval( + r#" +(() => { + const root = document.documentElement || document.appendChild(document.createElement('html')); + const body = document.body || root.appendChild(document.createElement('body')); + const form = document.createElement('form'); + const parentSelect = document.createElement('select'); + const childSelect = document.createElement('select'); + form.append(parentSelect); + parentSelect.append(childSelect); + body.append(form); + + const normalOption = document.createElement('option'); + childSelect.append(normalOption); + const nestedOption = document.createElement('option'); + normalOption.append(nestedOption); + + const div = document.createElement('div'); + childSelect.append(div); + const divOption = document.createElement('option'); + div.append(divOption); + + const hr = document.createElement('hr'); + childSelect.append(hr); + const hrOption = document.createElement('option'); + hr.append(hrOption); + + const datalist = document.createElement('datalist'); + childSelect.append(datalist); + const datalistOption = document.createElement('option'); + datalist.append(datalistOption); + + const optgroup = document.createElement('optgroup'); + childSelect.append(optgroup); + const optgroupOption = document.createElement('option'); + optgroup.append(optgroupOption); + const nestedOptgroup = document.createElement('optgroup'); + optgroup.append(nestedOptgroup); + const nestedOptgroupOption = document.createElement('option'); + nestedOptgroup.append(nestedOptgroupOption); + + normalOption.selected = true; + datalistOption.selected = true; + + return JSON.stringify({ + parentLength: parentSelect.length, + childLength: childSelect.length, + options: Array.from(childSelect.options, option => + option === normalOption ? 'normal' : + option === divOption ? 'div' : + option === optgroupOption ? 'optgroup' : 'unexpected'), + validForms: [normalOption, divOption, optgroupOption].map(option => option.form === form), + invalidForms: [nestedOption, hrOption, datalistOption, nestedOptgroupOption] + .map(option => option.form === null), + indices: [normalOption, divOption, optgroupOption, nestedOption, hrOption, + datalistOption, nestedOptgroupOption].map(option => option.index), + normalStillSelected: normalOption.selected + }); +})() +"#, + ) + .expect("option nearest ancestor select behavior should evaluate"); + + assert_eq!( + result, + r#"{"parentLength":0,"childLength":3,"options":["normal","div","optgroup"],"validForms":[true,true,true],"invalidForms":[true,true,true,true],"indices":[0,1,2,0,0,0,0],"normalStillSelected":true}"# + ); +} + #[test] fn child_document_select_exposes_detached_mutation_surface() { let mut vm = new_storage_test_vm("https://child-select-options-surface.test/");