mirror of
https://github.com/lexmount/moli.git
synced 2026-09-24 00:01:27 +00:00
fix(forms): synchronize selectedcontent clones
This commit is contained in:
@@ -170,6 +170,14 @@ impl DomHost {
|
||||
self.dom.optgroup_nearest_ancestor_select(handle)
|
||||
}
|
||||
|
||||
pub fn selectedcontent_nearest_ancestor_select(&self, handle: DomHandle) -> Option<DomHandle> {
|
||||
self.dom.selectedcontent_nearest_ancestor_select(handle)
|
||||
}
|
||||
|
||||
pub fn select_selectedcontent_elements(&self, handle: DomHandle) -> Vec<DomHandle> {
|
||||
self.dom.select_selectedcontent_elements(handle)
|
||||
}
|
||||
|
||||
pub fn option_is_disabled(&self, handle: DomHandle) -> bool {
|
||||
self.dom.option_is_disabled(handle)
|
||||
}
|
||||
|
||||
@@ -1603,6 +1603,50 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selectedcontent_owner_rejects_recursive_clone_boundaries() {
|
||||
let url = url::Url::parse("https://selectedcontent-owner.test/").unwrap();
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
||||
host.reset_html_document_shell();
|
||||
let body = host.document_body_handle().unwrap();
|
||||
|
||||
let select = host.create_element("select");
|
||||
let button = host.create_element("button");
|
||||
let selectedcontent = host.create_element("selectedcontent");
|
||||
assert!(host.append_child(body, select));
|
||||
assert!(host.append_child(select, button));
|
||||
assert!(host.append_child(button, selectedcontent));
|
||||
assert_eq!(
|
||||
host.selectedcontent_nearest_ancestor_select(selectedcontent),
|
||||
Some(select)
|
||||
);
|
||||
|
||||
let option = host.create_element("option");
|
||||
let option_selectedcontent = host.create_element("selectedcontent");
|
||||
assert!(host.append_child(select, option));
|
||||
assert!(host.append_child(option, option_selectedcontent));
|
||||
|
||||
let nested_selectedcontent = host.create_element("selectedcontent");
|
||||
assert!(host.append_child(selectedcontent, nested_selectedcontent));
|
||||
|
||||
let nested_select = host.create_element("select");
|
||||
let nested_select_selectedcontent = host.create_element("selectedcontent");
|
||||
assert!(host.append_child(select, nested_select));
|
||||
assert!(host.append_child(nested_select, nested_select_selectedcontent));
|
||||
|
||||
for rejected in [
|
||||
option_selectedcontent,
|
||||
nested_selectedcontent,
|
||||
nested_select_selectedcontent,
|
||||
] {
|
||||
assert_eq!(host.selectedcontent_nearest_ancestor_select(rejected), None);
|
||||
}
|
||||
assert_eq!(
|
||||
host.select_selectedcontent_elements(select),
|
||||
vec![selectedcontent]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_option_insertion_deselects_single_select_peers_without_dirtying_them() {
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
|
||||
|
||||
@@ -95,6 +95,55 @@ impl NativeDom {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn selectedcontent_nearest_ancestor_select(
|
||||
&self,
|
||||
selectedcontent_id: NativeNodeId,
|
||||
) -> Option<NativeNodeId> {
|
||||
if !self
|
||||
.node(selectedcontent_id)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| element.is_html_element("selectedcontent"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut nearest_select = None;
|
||||
let mut current = self.parent_node(selectedcontent_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_option() || element.is_html_element("selectedcontent") {
|
||||
return None;
|
||||
}
|
||||
if element.is_html_select() {
|
||||
if nearest_select.is_some() {
|
||||
return None;
|
||||
}
|
||||
nearest_select = Some(parent);
|
||||
}
|
||||
current = self.parent_node(parent);
|
||||
}
|
||||
nearest_select
|
||||
}
|
||||
|
||||
pub fn select_selectedcontent_elements(&self, select_id: NativeNodeId) -> Vec<NativeNodeId> {
|
||||
if !self
|
||||
.node(select_id)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(Element::is_html_select)
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
self.elements_by_tag_name(select_id, "selectedcontent", false)
|
||||
.into_iter()
|
||||
.filter(|selectedcontent| {
|
||||
self.selectedcontent_nearest_ancestor_select(*selectedcontent) == Some(select_id)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn nearest_ancestor_select(
|
||||
&self,
|
||||
element_id: NativeNodeId,
|
||||
|
||||
@@ -1405,6 +1405,12 @@ impl ParseHandle {
|
||||
})
|
||||
}
|
||||
|
||||
fn is_html_option_element(&self) -> bool {
|
||||
self.element_name.as_ref().is_some_and(|name| {
|
||||
name.local.as_ref() == "option" && name.ns.as_ref() == "http://www.w3.org/1999/xhtml"
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn node_id(&self) -> NativeNodeId {
|
||||
self.dom_node_id()
|
||||
.expect("parser operation requires a real DOM node handle")
|
||||
@@ -1808,10 +1814,23 @@ impl TreeSink for DocumentSink {
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_clone_an_option_into_selectedcontent(&self, option: &Self::Handle) {
|
||||
if let Some(option_id) = option.dom_node_id() {
|
||||
self.target
|
||||
.borrow_mut()
|
||||
.maybe_clone_an_option_into_selectedcontent(option_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn pop(&self, node: &Self::Handle) {
|
||||
// html5ever calls `pop()` when the element has been fully closed by the parser. For
|
||||
// classic parser-inserted scripts that is the earliest safe moment to hand execution back
|
||||
// to the runtime without risking partial inline source or a half-built element subtree.
|
||||
// Its explicit `</option>` path invokes the selectedcontent hook itself, but implicit
|
||||
// stack pops (including EOF) only arrive here, so route those through the same hook.
|
||||
if node.is_html_option_element() {
|
||||
self.maybe_clone_an_option_into_selectedcontent(node);
|
||||
}
|
||||
if let Some(node_id) = node.dom_node_id() {
|
||||
self.target
|
||||
.borrow_mut()
|
||||
|
||||
@@ -628,6 +628,8 @@ pub trait ParserDomMutationConsumer {
|
||||
|
||||
fn finish_parsing_link_children(&mut self, node_id: NativeNodeId);
|
||||
|
||||
fn maybe_clone_an_option_into_selectedcontent(&mut self, _node_id: NativeNodeId) {}
|
||||
|
||||
fn attach_declarative_shadow_for_parser(
|
||||
&mut self,
|
||||
host_id: NativeNodeId,
|
||||
@@ -659,6 +661,7 @@ struct ParserDomMutationSink {
|
||||
mark_script_already_started_for_parser: unsafe fn(NonNull<()>, NativeNodeId),
|
||||
finish_parsing_script_children: unsafe fn(NonNull<()>, NativeNodeId),
|
||||
finish_parsing_link_children: unsafe fn(NonNull<()>, NativeNodeId),
|
||||
maybe_clone_an_option_into_selectedcontent: unsafe fn(NonNull<()>, NativeNodeId),
|
||||
attach_declarative_shadow_for_parser:
|
||||
unsafe fn(NonNull<()>, NativeNodeId, NativeNodeId, Vec<NativeAttribute>) -> bool,
|
||||
associate_parser_form_owner: unsafe fn(NonNull<()>, NativeNodeId, NativeNodeId) -> bool,
|
||||
@@ -817,6 +820,15 @@ impl ParserDomMutationSink {
|
||||
// pointed-to consumer to remain live and exclusive for the pump step.
|
||||
unsafe { data.cast::<T>().as_mut() }.finish_parsing_link_children(node_id);
|
||||
}
|
||||
unsafe fn maybe_clone_an_option_into_selectedcontent_impl<T: ParserDomMutationConsumer>(
|
||||
data: NonNull<()>,
|
||||
node_id: NativeNodeId,
|
||||
) {
|
||||
// SAFETY: ParserDomMutationSink::from_consumer requires the
|
||||
// pointed-to consumer to remain live and exclusive for the pump step.
|
||||
unsafe { data.cast::<T>().as_mut() }
|
||||
.maybe_clone_an_option_into_selectedcontent(node_id);
|
||||
}
|
||||
unsafe fn attach_declarative_shadow_for_parser_impl<T: ParserDomMutationConsumer>(
|
||||
data: NonNull<()>,
|
||||
host_id: NativeNodeId,
|
||||
@@ -859,6 +871,8 @@ impl ParserDomMutationSink {
|
||||
mark_script_already_started_for_parser: mark_script_already_started_for_parser_impl::<T>,
|
||||
finish_parsing_script_children: finish_parsing_script_children_impl::<T>,
|
||||
finish_parsing_link_children: finish_parsing_link_children_impl::<T>,
|
||||
maybe_clone_an_option_into_selectedcontent:
|
||||
maybe_clone_an_option_into_selectedcontent_impl::<T>,
|
||||
attach_declarative_shadow_for_parser: attach_declarative_shadow_for_parser_impl::<T>,
|
||||
associate_parser_form_owner: associate_parser_form_owner_impl::<T>,
|
||||
}
|
||||
@@ -981,6 +995,12 @@ impl ParserDomMutationSink {
|
||||
unsafe { (self.finish_parsing_link_children)(self.data, node_id) };
|
||||
}
|
||||
|
||||
fn maybe_clone_an_option_into_selectedcontent(self, node_id: NativeNodeId) {
|
||||
// SAFETY: construction ties the raw pointer and callback to the same
|
||||
// consumer remains live for the current runtime-DOM sink step.
|
||||
unsafe { (self.maybe_clone_an_option_into_selectedcontent)(self.data, node_id) };
|
||||
}
|
||||
|
||||
fn attach_declarative_shadow_for_parser(
|
||||
self,
|
||||
host_id: NativeNodeId,
|
||||
@@ -2864,6 +2884,14 @@ impl ParserStreamHtmlTreeSinkTarget {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn maybe_clone_an_option_into_selectedcontent(&mut self, node_id: NativeNodeId) {
|
||||
if let Some(owner) = &self.runtime_dom_sinks {
|
||||
owner
|
||||
.dom_mutation_sink()
|
||||
.maybe_clone_an_option_into_selectedcontent(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_declarative_shadow_for_dom_host(
|
||||
&mut self,
|
||||
host_id: NativeNodeId,
|
||||
|
||||
@@ -322,6 +322,12 @@ impl ParserDomMutationConsumer for DocumentWriteParserMutationOwner<'_, '_, '_>
|
||||
.finish_parsing_link_children(node_id);
|
||||
}
|
||||
|
||||
fn maybe_clone_an_option_into_selectedcontent(&mut self, node_id: DomHandle) {
|
||||
let _ = self
|
||||
.runtime
|
||||
.sync_selectedcontents_after_parser_option_finished(self.scope, self.host_ptr, node_id);
|
||||
}
|
||||
|
||||
fn attach_declarative_shadow_for_parser(
|
||||
&mut self,
|
||||
host_id: DomHandle,
|
||||
|
||||
@@ -13,6 +13,7 @@ use moli_selector::stylo_flat_tree_heading_descendants;
|
||||
use super::*;
|
||||
|
||||
mod details;
|
||||
mod selectedcontent;
|
||||
mod tree;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1769,8 +1770,15 @@ impl DocumentRuntime {
|
||||
.set_select_explicit_none_state(handle, explicit_none)
|
||||
}
|
||||
|
||||
pub(crate) fn set_select_value(&mut self, handle: DomHandle, value: &str) -> bool {
|
||||
self.dom_host.set_select_value(handle, value)
|
||||
pub(crate) fn set_select_value(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
handle: DomHandle,
|
||||
value: &str,
|
||||
) -> bool {
|
||||
let changed = self.dom_host.set_select_value(handle, value);
|
||||
self.sync_selectedcontents_for_select_in_reaction_scope(scope, host_ptr, handle) || changed
|
||||
}
|
||||
|
||||
pub(crate) fn set_script_async(
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
use crate::{
|
||||
custom_elements,
|
||||
document_runtime::{DocumentRuntime, DomHandle},
|
||||
dom::native::Node,
|
||||
native_bridge::JsContextHost,
|
||||
};
|
||||
|
||||
impl DocumentRuntime {
|
||||
pub(crate) fn sync_selectedcontents_for_select_in_reaction_scope(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
select: DomHandle,
|
||||
) -> bool {
|
||||
custom_elements::with_custom_element_reaction_scope(scope, host_ptr, |scope| {
|
||||
self.sync_selectedcontents_for_select_appending_to_current_reaction_queue(
|
||||
scope, host_ptr, select,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn sync_selectedcontents_after_parser_option_finished(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
option: DomHandle,
|
||||
) -> bool {
|
||||
let Some(select) = self.dom_host.option_nearest_ancestor_select(option) else {
|
||||
return false;
|
||||
};
|
||||
if self
|
||||
.dom_host
|
||||
.select_selected_option_elements(select)
|
||||
.first()
|
||||
.copied()
|
||||
!= Some(option)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.sync_selectedcontents_for_select_appending_to_current_reaction_queue(
|
||||
scope, host_ptr, select,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn sync_selectedcontents_for_select_appending_to_current_reaction_queue(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
select: DomHandle,
|
||||
) -> bool {
|
||||
let Some(select_element) = self.dom_host.node(select).and_then(Node::as_element) else {
|
||||
return false;
|
||||
};
|
||||
if !select_element.is_html_select() || select_element.has_attribute("multiple") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let selected_option = self
|
||||
.dom_host
|
||||
.select_selected_option_elements(select)
|
||||
.first()
|
||||
.copied();
|
||||
let targets = self.dom_host.select_selectedcontent_elements(select);
|
||||
let mut changed = false;
|
||||
for target in targets {
|
||||
changed |= self.clone_selected_option_contents_into_selectedcontent(
|
||||
scope,
|
||||
host_ptr,
|
||||
target,
|
||||
selected_option,
|
||||
);
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn clone_selected_option_contents_into_selectedcontent(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
selectedcontent: DomHandle,
|
||||
selected_option: Option<DomHandle>,
|
||||
) -> bool {
|
||||
let Some(document) = self.dom_host.owner_document_handle(selectedcontent) else {
|
||||
return false;
|
||||
};
|
||||
let fragment = self.create_document_fragment_for_document(document);
|
||||
if let Some(option) = selected_option {
|
||||
let source_children = self.dom_host.child_handles(option).collect::<Vec<_>>();
|
||||
for source_child in source_children {
|
||||
let Some(clone) = self.clone_node(scope, host_ptr, source_child, true) else {
|
||||
return false;
|
||||
};
|
||||
if !self
|
||||
.dom_host
|
||||
.append_child_without_mutation_effects(fragment, clone)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let existing_children = self
|
||||
.dom_host
|
||||
.child_handles(selectedcontent)
|
||||
.collect::<Vec<_>>();
|
||||
self.replace_all_children_with_fragment_appending_to_current_reaction_queue(
|
||||
scope,
|
||||
host_ptr,
|
||||
selectedcontent,
|
||||
fragment,
|
||||
&existing_children,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -467,6 +467,13 @@ impl ParserDomMutationConsumer for ChildFrameLiveParserOwner<'_, '_, '_> {
|
||||
.finish_parsing_link_children(node_id);
|
||||
}
|
||||
|
||||
fn maybe_clone_an_option_into_selectedcontent(&mut self, node_id: DomHandle) {
|
||||
let host_ptr = self.host as *mut JsContextHost;
|
||||
let _ = self
|
||||
.host
|
||||
.sync_selectedcontents_after_parser_option_finished(self.scope, host_ptr, node_id);
|
||||
}
|
||||
|
||||
fn attach_declarative_shadow_for_parser(
|
||||
&mut self,
|
||||
host_id: DomHandle,
|
||||
|
||||
@@ -697,7 +697,7 @@ pub(in crate::native_bridge) fn select_value_setter_function<'s>(
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_select_value(handle, &next_value);
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_select_value(scope, runtime_ptr, handle, &next_value);
|
||||
rv.set_undefined();
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ use self::parser_turn::{PageTaskTurnResult, ParserDriver};
|
||||
#[cfg(test)]
|
||||
use self::parser_turn::{
|
||||
ParserStepAdvanceOutcome, ScriptHandoffOutcome, bind_parser_owned_script_handle,
|
||||
finish_parser_session_for_test,
|
||||
};
|
||||
pub(super) use self::pending_residence::{PendingPhaseOneResidence, PendingPhaseOneResumeOutcome};
|
||||
pub(super) use self::state::ConcurrentParseTimeRuntime;
|
||||
@@ -440,6 +441,23 @@ mod tests {
|
||||
pub(super) async fn parse_phase_one_html_into_page_vm_for_test_with_env(
|
||||
html: &'static str,
|
||||
env: PageVmEnvConfig,
|
||||
) -> PageVm {
|
||||
parse_phase_one_html_into_page_vm_for_test_with_env_and_finish(html, env, false).await
|
||||
}
|
||||
|
||||
async fn parse_finished_phase_one_html_into_page_vm_for_test(html: &'static str) -> PageVm {
|
||||
parse_phase_one_html_into_page_vm_for_test_with_env_and_finish(
|
||||
html,
|
||||
default_test_page_vm_env_config(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn parse_phase_one_html_into_page_vm_for_test_with_env_and_finish(
|
||||
html: &'static str,
|
||||
env: PageVmEnvConfig,
|
||||
finish_after_step: bool,
|
||||
) -> PageVm {
|
||||
let PhaseOnePageVmHarness {
|
||||
mut page_vm,
|
||||
@@ -472,6 +490,12 @@ mod tests {
|
||||
.await
|
||||
.expect("parser step should complete");
|
||||
assert!(matches!(outcome, ParserStepAdvanceOutcome::Continue));
|
||||
if finish_after_step {
|
||||
driver.parser_session.request_finish();
|
||||
page_vm.vm_mut().with_dom_host_parse_step(|vm| {
|
||||
finish_parser_session_for_test(driver.parser_session, vm)
|
||||
});
|
||||
}
|
||||
page_vm
|
||||
}
|
||||
|
||||
@@ -12411,6 +12435,113 @@ JSON.stringify({
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_option_finish_and_select_value_sync_selectedcontent_clones() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("current-thread runtime should build");
|
||||
|
||||
runtime.block_on(tokio::task::LocalSet::new().run_until(async move {
|
||||
let mut page_vm = parse_phase_one_html_into_page_vm_for_test(
|
||||
r#"<!doctype html><html><body>
|
||||
<select id="select">
|
||||
<button><selectedcontent id="selectedcontent">default</selectedcontent></button>
|
||||
<div><option id="one"><span id="source-span">one</span></option></div>
|
||||
<div><option id="two"><strong>two</strong></option></div>
|
||||
</select>
|
||||
<script>
|
||||
const select = document.getElementById('select');
|
||||
const selectedcontent = document.getElementById('selectedcontent');
|
||||
const sourceSpan = document.querySelector('#one > span');
|
||||
window.selectedcontentState = [
|
||||
selectedcontent.textContent.trim(),
|
||||
selectedcontent.firstElementChild !== sourceSpan,
|
||||
];
|
||||
select.value = 'two';
|
||||
window.selectedcontentState.push(
|
||||
selectedcontent.textContent.trim(),
|
||||
selectedcontent.firstElementChild.tagName,
|
||||
);
|
||||
</script>
|
||||
</body></html>"#,
|
||||
)
|
||||
.await;
|
||||
|
||||
let result = page_vm
|
||||
.evaluate_expression("JSON.stringify(window.selectedcontentState)")
|
||||
.expect("selectedcontent parser state should evaluate");
|
||||
assert_eq!(
|
||||
result.get("value").and_then(serde_json::Value::as_str),
|
||||
Some(r#"["one",true,"two","STRONG"]"#),
|
||||
"parser option completion and select.value must synchronously clone the selected option children"
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_eof_option_finish_syncs_selectedcontent_clones() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("current-thread runtime should build");
|
||||
|
||||
runtime.block_on(tokio::task::LocalSet::new().run_until(async move {
|
||||
let mut text_page_vm = parse_finished_phase_one_html_into_page_vm_for_test(
|
||||
r#"<select><button><selectedcontent></button><option>X"#,
|
||||
)
|
||||
.await;
|
||||
let text_result = text_page_vm
|
||||
.evaluate_expression(
|
||||
r#"
|
||||
(() => {
|
||||
const selectedcontent = document.querySelector('selectedcontent');
|
||||
const source = document.querySelector('option');
|
||||
return [
|
||||
selectedcontent.textContent,
|
||||
selectedcontent.firstChild !== source.firstChild
|
||||
].join('|');
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("EOF-closed text option selectedcontent state should evaluate");
|
||||
assert_eq!(
|
||||
text_result.get("value").and_then(serde_json::Value::as_str),
|
||||
Some("X|true"),
|
||||
"EOF-closing an option must clone its text into selectedcontent"
|
||||
);
|
||||
|
||||
let mut nested_page_vm = parse_finished_phase_one_html_into_page_vm_for_test(
|
||||
r#"<select><button><selectedcontent></button><option>x<i>i<b>ib</i>b"#,
|
||||
)
|
||||
.await;
|
||||
let nested_result = nested_page_vm
|
||||
.evaluate_expression(
|
||||
r#"
|
||||
(() => {
|
||||
const selectedcontent = document.querySelector('selectedcontent');
|
||||
const source = document.querySelector('option');
|
||||
return [
|
||||
selectedcontent.textContent,
|
||||
selectedcontent.innerHTML === source.innerHTML,
|
||||
selectedcontent.firstChild !== source.firstChild,
|
||||
selectedcontent.querySelector('i') !== source.querySelector('i'),
|
||||
selectedcontent.querySelectorAll('b').length
|
||||
].join('|');
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("EOF-closed nested option selectedcontent state should evaluate");
|
||||
assert_eq!(
|
||||
nested_result
|
||||
.get("value")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("xiibb|true|true|true|2"),
|
||||
"EOF-closing an option must deep-clone its parsed children into selectedcontent"
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_merged_root_attributes_hide_nonce_content_values() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
|
||||
@@ -346,6 +346,12 @@ impl ParserDomMutationConsumer for PhaseOneParserOwner<'_> {
|
||||
.finish_parsing_link_children(node_id);
|
||||
}
|
||||
|
||||
fn maybe_clone_an_option_into_selectedcontent(&mut self, node_id: NativeNodeId) {
|
||||
let _ = self
|
||||
.vm
|
||||
.sync_selectedcontents_after_parser_option_finished_in_default_context(node_id);
|
||||
}
|
||||
|
||||
fn attach_declarative_shadow_for_parser(
|
||||
&mut self,
|
||||
host_id: NativeNodeId,
|
||||
@@ -387,6 +393,15 @@ impl ParserElementCreationConsumer for PhaseOneParserOwner<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn finish_parser_session_for_test(
|
||||
parser_session: &mut DocumentParserSession,
|
||||
vm: &mut ScriptVm,
|
||||
) {
|
||||
let mut parser_owner = PhaseOneParserOwner { vm };
|
||||
let _ = parser_session.finish(&mut parser_owner);
|
||||
}
|
||||
|
||||
pub(super) enum PageTaskTurnResult {
|
||||
/// No parse-visible task was runnable.
|
||||
NoTask,
|
||||
|
||||
@@ -5816,6 +5816,17 @@ impl ScriptVm {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn sync_selectedcontents_after_parser_option_finished_in_default_context(
|
||||
&mut self,
|
||||
option: NativeNodeId,
|
||||
) -> Result<()> {
|
||||
self.with_default_context_scope(|scope, host_ptr| {
|
||||
unsafe { &mut *host_ptr }
|
||||
.sync_selectedcontents_after_parser_option_finished(scope, host_ptr, option);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn apply_parser_created_null_registry_associations_in_default_context(
|
||||
&mut self,
|
||||
handles: &[NativeNodeId],
|
||||
|
||||
@@ -6577,6 +6577,71 @@ async fn element_matches_delegates_loaded_child_document_elements() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn child_parser_eof_syncs_selectedcontent_for_navigation_and_document_write() {
|
||||
let mut vm = new_storage_test_vm("https://child-selectedcontent-parser.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const frame = document.createElement("iframe");
|
||||
frame.srcdoc = "<select><button><selectedcontent></button><option>X";
|
||||
(document.body || document.documentElement || document).appendChild(frame);
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("child selectedcontent navigation setup should evaluate");
|
||||
run_child_navigation_commit_and_host_load_for_test(&mut vm, "child selectedcontent navigation")
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const doc = document.querySelector("iframe").contentDocument;
|
||||
const selectedcontent = doc.querySelector("selectedcontent");
|
||||
const source = doc.querySelector("option");
|
||||
return [selectedcontent.textContent, selectedcontent.firstChild !== source.firstChild].join("|");
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("child selectedcontent navigation state should evaluate"),
|
||||
"X|true"
|
||||
);
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const doc = document.querySelector("iframe").contentDocument;
|
||||
doc.open();
|
||||
doc.write("<select><button><selectedcontent></button><option>x<i>i<b>ib</i>b");
|
||||
doc.close();
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("child selectedcontent document.write setup should evaluate");
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const doc = document.querySelector("iframe").contentDocument;
|
||||
const selectedcontent = doc.querySelector("selectedcontent");
|
||||
const source = doc.querySelector("option");
|
||||
return [
|
||||
selectedcontent.textContent,
|
||||
selectedcontent.innerHTML === source.innerHTML,
|
||||
selectedcontent.firstChild !== source.firstChild,
|
||||
selectedcontent.querySelector("i") !== source.querySelector("i"),
|
||||
selectedcontent.querySelectorAll("b").length
|
||||
].join("|");
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("child selectedcontent document.write state should evaluate"),
|
||||
"xiibb|true|true|true|2"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn child_navigation_performance_name_updates_after_iframe_src_change() {
|
||||
let mut vm = new_storage_test_vm("https://child-navigation-performance.test/page.html");
|
||||
|
||||
Reference in New Issue
Block a user