From 830ae70b886cae90d3bebca1dbfaabb73fac7107 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 2 Sep 2026 16:34:17 +0800 Subject: [PATCH] fix(forms): expose selectedcontent inner text without a box --- .../src/native_bridge/element/content.rs | 24 ++++++++++++++++ .../tree_mutations_and_document_write.rs | 3 +- .../tests/dom_xhr/dom/rendered_text.rs | 28 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/moli-renderer-v8/src/native_bridge/element/content.rs b/moli-renderer-v8/src/native_bridge/element/content.rs index 39e272995e..be90bbb267 100644 --- a/moli-renderer-v8/src/native_bridge/element/content.rs +++ b/moli-renderer-v8/src/native_bridge/element/content.rs @@ -13,6 +13,7 @@ use super::super::{ JsContextHost, document::set_detached_text_replacement_value, node::node_runtime_and_handle_from_object_or_detached, throw_dom_exception, }; +use super::geometry::observable_client_rects; use super::{ html_element_getter_receiver, html_element_setter_receiver, observable_sources_with_fragments, property_string_value, @@ -817,6 +818,29 @@ fn node_inner_text( if rendered_state.box_state() == ElementBoxState::NoBox { return Ok(node.text_content(runtime.dom_host().dom())); } + // The lightweight rendered-state path derives box presence from computed + // display values. A selectedcontent element can still have no box when + // its native select ancestor suppresses author children. Consult the + // actual layout surface for this control-specific boundary so innerText + // follows its required textContent fallback. Once a customizable select + // produces real child boxes, its non-empty rect list keeps it on the + // normal rendered-text path. + if runtime + .dom_host() + .selectedcontent_nearest_ancestor_select(handle) + .is_some() + && rendered_state + .target_style() + .is_some_and(|style| style.display != ComputedDisplayKind::Contents) + && observable_client_rects( + runtime, + handle, + moli_layout::LayoutFlushReason::SynchronousGeometry, + )? + .is_empty() + { + return Ok(node.text_content(runtime.dom_host().dom())); + } let mut writer = InnerTextWriter::default(); append_inner_text( diff --git a/moli-renderer-v8/src/runtime/phase_one/tests/extracted/tree_mutations_and_document_write.rs b/moli-renderer-v8/src/runtime/phase_one/tests/extracted/tree_mutations_and_document_write.rs index 68118d29eb..6da6f55d18 100644 --- a/moli-renderer-v8/src/runtime/phase_one/tests/extracted/tree_mutations_and_document_write.rs +++ b/moli-renderer-v8/src/runtime/phase_one/tests/extracted/tree_mutations_and_document_write.rs @@ -1446,6 +1446,7 @@ const selectedcontent = document.getElementById('selectedcontent'); const sourceSpan = document.querySelector('#one > span'); window.selectedcontentState = [ selectedcontent.textContent.trim(), + selectedcontent.innerText.trim(), selectedcontent.firstElementChild !== sourceSpan, ]; select.value = 'two'; @@ -1468,7 +1469,7 @@ window.selectedcontentState.push(selectedcontent.textContent.trim()); .expect("selectedcontent parser state should evaluate"); assert_eq!( result.get("value").and_then(serde_json::Value::as_str), - Some(r#"["one",true,"two","STRONG","updated","one"]"#), + Some(r#"["one","one",true,"two","STRONG","updated","one"]"#), "parser option completion and select setters must synchronously clone the selected option children" ); })); diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/rendered_text.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/rendered_text.rs index 2ab5d68014..cbfa914e37 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/rendered_text.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/rendered_text.rs @@ -89,6 +89,34 @@ fn dom_parser_inner_text_is_html_only() { ); } +#[test] +fn selectedcontent_inner_text_falls_back_when_its_select_child_box_is_suppressed() { + let mut vm = new_parsed_test_vm( + "https://selectedcontent-inner-text.test/", + "
before
", + ); + + let result = vm + .eval( + r#" +void document.body.innerText; +const select = document.createElement('select'); +select.innerHTML = ''; +select.style.appearance = 'base-select'; +document.body.append(select); +const selectedcontent = document.getElementById('selectedcontent'); +[ + selectedcontent.textContent, + selectedcontent.innerText, + selectedcontent.getClientRects().length +].join('|') +"#, + ) + .expect("selectedcontent innerText should evaluate"); + + assert_eq!(result, "one|one|0"); +} + #[test] fn live_inner_text_applies_inline_text_transform() { let mut vm = new_rendered_test_vm(