fix(forms): align selectedcontent mutation timing

This commit is contained in:
ldm0
2026-09-09 06:38:20 +08:00
parent 225ae8f7aa
commit f231cf123f
17 changed files with 604 additions and 48 deletions
@@ -1819,6 +1819,24 @@ impl DocumentRuntime {
options: RuntimeMutationOptions,
prepublished_removals: Vec<devtools_mutations::DevToolsDomPrepublishedRemoval>,
) -> bool {
let result = self.apply_runtime_mutation_effects_before_runtime_followups(
scope,
host_ptr,
effects,
options,
prepublished_removals,
);
finish_runtime_mutation_effects(self, scope, host_ptr, result)
}
pub(super) fn apply_runtime_mutation_effects_before_runtime_followups(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
effects: DomMutationEffects,
options: RuntimeMutationOptions,
prepublished_removals: Vec<devtools_mutations::DevToolsDomPrepublishedRemoval>,
) -> RuntimeMutationApplyResult {
let mut result = apply_runtime_mutation_effects_to_dom_host(
&mut self.mutations,
&self.document,
@@ -1834,7 +1852,7 @@ impl DocumentRuntime {
&mut result.devtools_dom_mutations,
prepublished_removals,
);
finish_runtime_mutation_effects(self, scope, host_ptr, result)
result
}
}
@@ -1851,6 +1869,12 @@ pub(super) struct RuntimeMutationApplyResult {
font_face_use_roots: Vec<DomHandle>,
}
impl RuntimeMutationApplyResult {
pub(super) fn did_change(&self) -> bool {
self.changed
}
}
#[derive(Clone, Debug)]
pub(super) struct InlineStyleAttributeCspMutation {
target: DomHandle,
@@ -1913,22 +1937,7 @@ pub(super) fn finish_runtime_mutation_effects(
}
for candidate in runtime_script_start_candidates {
let (node, host_script_handle) = candidate.into_parts();
let Some(plan) = runtime.host_plan_script_start(node, &host_script_handle) else {
continue;
};
match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(runtime, plan) {
Ok(Some(committed)) => {
execute_committed_inline_classic_script(runtime, scope, host_ptr, committed);
}
Ok(None) => {}
Err(message) => {
if let Some(message) = v8::String::new(scope, &message) {
let exception = v8::Exception::error(scope, message);
scope.throw_exception(exception);
}
}
}
finish_runtime_script_start_candidate(runtime, scope, host_ptr, candidate);
}
for popover in removed_open_popovers {
@@ -2011,6 +2020,30 @@ pub(super) fn finish_runtime_mutation_effects(
changed
}
pub(super) fn finish_runtime_script_start_candidate(
runtime: &mut DocumentRuntime,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
candidate: crate::mutation_coordinator::RuntimeScriptStartCandidate,
) {
let (node, host_script_handle) = candidate.into_parts();
let Some(plan) = runtime.host_plan_script_start(node, &host_script_handle) else {
return;
};
match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(runtime, plan) {
Ok(Some(committed)) => {
execute_committed_inline_classic_script(runtime, scope, host_ptr, committed);
}
Ok(None) => {}
Err(message) => {
if let Some(message) = v8::String::new(scope, &message) {
let exception = v8::Exception::error(scope, message);
scope.throw_exception(exception);
}
}
}
}
fn execute_committed_inline_classic_script(
runtime: &mut DocumentRuntime,
scope: &mut v8::PinScope<'_, '_>,
@@ -1,11 +1,234 @@
use super::{
finish_runtime_mutation_effects, finish_runtime_script_start_candidate,
tree::{TreeInsertionPlan, TreeInsertionPostConnectionStep, TreeReactionDispatchPolicy},
};
use crate::{
custom_elements,
document_runtime::{DocumentRuntime, DomHandle},
dom::native::Node,
dom::native::{DomMutationEffects, Node},
mutation_coordinator::{
ConnectedScriptMutationPolicy, RuntimeMutationOptions, ScriptStartRequest,
},
native_bridge::JsContextHost,
util::context_host_ptr_from_global_bridge,
};
fn selectedcontent_update_microtask_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments<'_>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return;
};
let Ok(value) = v8::Local::<v8::BigInt>::try_from(args.data()) else {
return;
};
let (index, lossless) = value.u64_value();
if !lossless {
return;
}
let select = DomHandle::new(index as usize);
if !unsafe { &mut *host_ptr }.take_pending_selectedcontent_update(select) {
return;
}
let runtime: &mut DocumentRuntime = unsafe { &mut *host_ptr };
let _ = runtime.sync_selectedcontents_for_select_in_reaction_scope(scope, host_ptr, select);
}
impl DocumentRuntime {
pub(super) fn apply_tree_insertion_mutation_effects_with_post_connection_steps(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
insertion_plan: &TreeInsertionPlan<'_>,
effects: DomMutationEffects,
options: RuntimeMutationOptions,
reaction_policy: TreeReactionDispatchPolicy,
run_post_connection_steps: bool,
prepublished_removals: Vec<super::devtools_mutations::DevToolsDomPrepublishedRemoval>,
) -> bool {
if !run_post_connection_steps
|| !insertion_plan.requires_interleaved_post_connection_steps()
{
let changed = self.apply_runtime_mutation_effects_with_prepublished_removals(
scope,
host_ptr,
effects,
options,
prepublished_removals,
);
if changed && run_post_connection_steps {
self.queue_selectedcontent_updates_after_selected_option_owner_change(
scope,
host_ptr,
&insertion_plan.selected_option_owners_before_insert,
);
}
return changed;
}
let run_script_steps = matches!(
options.connected_script_policy,
ConnectedScriptMutationPolicy::PrepareAndStart
);
let script_start_requests = if run_script_steps {
self.mutations
.plan_connected_script_start_requests(&mut self.dom_host, &effects)
} else {
Vec::new()
};
let result = self.apply_runtime_mutation_effects_before_runtime_followups(
scope,
host_ptr,
effects,
options.with_connected_script_policy(ConnectedScriptMutationPolicy::DeferToOwner),
prepublished_removals,
);
if result.did_change() {
self.queue_selectedcontent_updates_after_selected_option_owner_change(
scope,
host_ptr,
&insertion_plan.selected_option_owners_before_insert,
);
self.run_tree_insertion_post_connection_steps(
scope,
host_ptr,
insertion_plan,
script_start_requests,
reaction_policy,
);
}
finish_runtime_mutation_effects(self, scope, host_ptr, result)
}
pub(super) fn run_parser_tree_insertion_post_connection_steps(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
insertion_plan: &TreeInsertionPlan<'_>,
) {
if insertion_plan.requires_interleaved_post_connection_steps() {
self.run_tree_insertion_post_connection_steps(
scope,
host_ptr,
insertion_plan,
Vec::new(),
TreeReactionDispatchPolicy::AppendToCurrentQueue,
);
}
}
fn run_tree_insertion_post_connection_steps(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
insertion_plan: &TreeInsertionPlan<'_>,
script_start_requests: Vec<ScriptStartRequest>,
reaction_policy: TreeReactionDispatchPolicy,
) {
match reaction_policy {
TreeReactionDispatchPolicy::DispatchNow => {
custom_elements::with_custom_element_reaction_scope(scope, host_ptr, |scope| {
self.run_tree_insertion_post_connection_steps_appending_to_current_reaction_queue(
scope,
host_ptr,
insertion_plan,
script_start_requests,
);
});
}
TreeReactionDispatchPolicy::AppendToCurrentQueue => {
self.run_tree_insertion_post_connection_steps_appending_to_current_reaction_queue(
scope,
host_ptr,
insertion_plan,
script_start_requests,
);
}
}
}
fn run_tree_insertion_post_connection_steps_appending_to_current_reaction_queue(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
insertion_plan: &TreeInsertionPlan<'_>,
script_start_requests: Vec<ScriptStartRequest>,
) {
let script_step_handles = insertion_plan
.post_connection_steps
.iter()
.filter_map(|step| match step {
TreeInsertionPostConnectionStep::Script(handle) => Some(*handle),
_ => None,
})
.collect::<Vec<_>>();
let (extra_script_requests, mut subtree_script_requests): (Vec<_>, Vec<_>) =
script_start_requests
.into_iter()
.partition(|request| !script_step_handles.contains(&request.handle()));
// A connected script parent can be prepared by a child insertion even
// though it is outside the inserted subtree. It precedes every
// descendant post-connection step in tree order.
for request in extra_script_requests {
self.run_script_start_request(scope, host_ptr, request);
}
for step in insertion_plan.post_connection_steps.iter().copied() {
let handle = step.handle();
if !self.dom_host.is_connected(handle) {
continue;
}
match step {
TreeInsertionPostConnectionStep::Script(script) => {
let Some(index) = subtree_script_requests
.iter()
.position(|request| request.handle() == script)
else {
continue;
};
let request = subtree_script_requests.remove(index);
self.run_script_start_request(scope, host_ptr, request);
}
TreeInsertionPostConnectionStep::SelectedContent(selectedcontent) => {
let _ = self.sync_selectedcontent_after_post_connection(
scope,
host_ptr,
selectedcontent,
);
}
TreeInsertionPostConnectionStep::SelectedOption(option) => {
let _ = self.sync_selectedcontents_for_selected_option(scope, host_ptr, option);
}
}
}
}
fn run_script_start_request(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
request: ScriptStartRequest,
) {
if request.clears_force_async() {
let _ = self
.dom_host
.set_script_force_async(request.handle(), false);
}
let candidate = self.mutations.collect_connected_script_start_candidate(
scope,
host_ptr,
&mut self.dom_host,
request.handle(),
&self.document,
);
if let Some(candidate) = candidate {
finish_runtime_script_start_candidate(self, scope, host_ptr, candidate);
}
}
pub(crate) fn sync_selectedcontents_for_select_in_reaction_scope(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
@@ -24,6 +247,15 @@ impl DocumentRuntime {
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
option: DomHandle,
) -> bool {
self.sync_selectedcontents_for_selected_option(scope, host_ptr, option)
}
fn sync_selectedcontents_for_selected_option(
&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;
@@ -42,6 +274,125 @@ impl DocumentRuntime {
)
}
fn sync_selectedcontent_after_post_connection(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
selectedcontent: DomHandle,
) -> bool {
let Some(select) = self
.dom_host
.selectedcontent_nearest_ancestor_select(selectedcontent)
else {
return false;
};
if self
.dom_host
.node(select)
.and_then(Node::as_element)
.is_some_and(|element| element.has_attribute("multiple"))
{
return false;
}
let selected_option = self
.dom_host
.select_selected_option_elements(select)
.first()
.copied();
self.clone_selected_option_contents_into_selectedcontent(
scope,
host_ptr,
selectedcontent,
selected_option,
)
}
pub(super) fn queue_selectedcontent_update_microtask(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
select: DomHandle,
) {
if !unsafe { &mut *host_ptr }.mark_pending_selectedcontent_update(select) {
return;
}
let data = v8::BigInt::new_from_u64(scope, select.index() as u64).into();
let Some(callback) = v8::Function::builder(selectedcontent_update_microtask_callback)
.data(data)
.build(scope)
else {
let _ = unsafe { &mut *host_ptr }.take_pending_selectedcontent_update(select);
return;
};
scope.enqueue_microtask(callback);
}
pub(super) fn queue_selectedcontent_updates_after_tree_removal(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
selected_option_owners_before_remove: &[(DomHandle, DomHandle)],
) {
self.queue_selectedcontent_updates_after_selected_option_owner_change(
scope,
host_ptr,
selected_option_owners_before_remove,
);
}
fn queue_selectedcontent_updates_after_selected_option_owner_change(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
selected_option_owners_before_change: &[(DomHandle, DomHandle)],
) {
for &(option, previous_select) in selected_option_owners_before_change {
if self.dom_host.option_nearest_ancestor_select(option) != Some(previous_select) {
self.queue_selectedcontent_update_microtask(scope, host_ptr, previous_select);
}
}
}
pub(super) fn selected_option_owners_in_subtrees(
&self,
roots: &[DomHandle],
) -> Vec<(DomHandle, DomHandle)> {
let options = roots
.iter()
.flat_map(|root| {
self.dom_host
.collect_matching_elements(*root, true, |handle| {
self.dom_host.is_html_element_named(handle, "option")
})
})
.collect::<Vec<_>>();
let mut selected_option_by_select = Vec::new();
let mut owners = Vec::new();
for option in options {
let Some(select) = self.dom_host.option_nearest_ancestor_select(option) else {
continue;
};
let selected_option = if let Some((_, selected_option)) = selected_option_by_select
.iter()
.find(|(cached_select, _)| *cached_select == select)
{
*selected_option
} else {
let selected_option = self
.dom_host
.select_selected_option_elements(select)
.first()
.copied();
selected_option_by_select.push((select, selected_option));
selected_option
};
if selected_option == Some(option) {
owners.push((option, select));
}
}
owners
}
pub(crate) fn sync_selectedcontents_for_select_appending_to_current_reaction_queue(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
@@ -158,11 +158,14 @@ impl DocumentRuntime {
.map(|started| started.elapsed().as_micros())
.unwrap_or_default();
let mutation_started = cpu_profile_enabled.then(std::time::Instant::now);
let changed = self.apply_runtime_mutation_effects_with_prepublished_removals(
let changed = self.apply_tree_insertion_mutation_effects_with_post_connection_steps(
scope,
host_ptr,
&insertion_plan,
effects,
RuntimeMutationOptions::js_dom_api(),
source_profile.reaction_policy,
true,
prepublished_removals,
);
let mutation_us = mutation_started
@@ -410,11 +413,14 @@ impl DocumentRuntime {
if effects.did_change() {
self.apply_node_iterator_pre_remove_plans(host_ptr, &insertion_plan.node_iterator_plan);
}
let changed = self.apply_runtime_mutation_effects_with_prepublished_removals(
let changed = self.apply_tree_insertion_mutation_effects_with_post_connection_steps(
scope,
host_ptr,
&insertion_plan,
effects,
mutation_options,
source_profile.reaction_policy,
!dispatch_atomic_move_callbacks,
prepublished_removals,
);
if changed {
@@ -9,7 +9,24 @@ use crate::{
native_bridge::JsContextHost,
};
pub(super) struct TreeInsertionPlan<'a> {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in crate::document_runtime::mutation_commands) enum TreeInsertionPostConnectionStep {
Script(DomHandle),
SelectedContent(DomHandle),
SelectedOption(DomHandle),
}
impl TreeInsertionPostConnectionStep {
pub(in crate::document_runtime::mutation_commands) fn handle(self) -> DomHandle {
match self {
Self::Script(handle) | Self::SelectedContent(handle) | Self::SelectedOption(handle) => {
handle
}
}
}
}
pub(in crate::document_runtime::mutation_commands) struct TreeInsertionPlan<'a> {
pub(super) parent: DomHandle,
pub(super) insertion_roots: &'a [DomHandle],
pub(super) inserting_fragment_children: bool,
@@ -23,6 +40,10 @@ pub(super) struct TreeInsertionPlan<'a> {
pub(super) image_relevant_mutation_plan: ImageRelevantMutationPlan,
pub(super) media_relevant_mutation_plan: MediaRelevantMutationPlan,
pub(super) option_selectedness_before_insert: Option<Vec<(DomHandle, bool)>>,
pub(in crate::document_runtime::mutation_commands) selected_option_owners_before_insert:
Vec<(DomHandle, DomHandle)>,
pub(in crate::document_runtime::mutation_commands) post_connection_steps:
Vec<TreeInsertionPostConnectionStep>,
}
#[derive(Clone, Copy)]
@@ -48,6 +69,13 @@ impl TreeInsertionPlan<'_> {
pub(super) fn was_lifecycle_connected_before_insert(&self) -> bool {
!self.lifecycle_connected_roots_before_insert.is_empty()
}
pub(in crate::document_runtime::mutation_commands) fn requires_interleaved_post_connection_steps(
&self,
) -> bool {
self.post_connection_steps
.iter()
.any(|step| !matches!(step, TreeInsertionPostConnectionStep::Script(_)))
}
}
impl TreeInsertionLiveRangeMode {
@@ -130,6 +158,12 @@ impl DocumentRuntime {
let option_selectedness_before_insert = (options.selectedness_policy.captures()
&& subtree_plan.may_have_options)
.then(|| self.option_selectedness_before_insert(insertion_roots));
let selected_option_owners_before_insert = if subtree_plan.may_have_options {
self.selected_option_owners_in_subtrees(insertion_roots)
} else {
Vec::new()
};
let post_connection_steps = self.tree_insertion_post_connection_steps(insertion_roots);
let adoption = self.tree_adoption_plan_before_insert(host_ptr, insertion_roots, parent);
let lifecycle_connected_roots_before_insert: Vec<DomHandle> = insertion_roots
.iter()
@@ -154,6 +188,36 @@ impl DocumentRuntime {
image_relevant_mutation_plan,
media_relevant_mutation_plan,
option_selectedness_before_insert,
selected_option_owners_before_insert,
post_connection_steps,
}
}
fn tree_insertion_post_connection_steps(
&self,
roots: &[DomHandle],
) -> Vec<TreeInsertionPostConnectionStep> {
let mut stack = roots.iter().rev().copied().collect::<Vec<_>>();
let mut steps = Vec::new();
while let Some(handle) = stack.pop() {
let Some(node) = self.dom_host.node(handle) else {
continue;
};
if node.is_script_element() {
steps.push(TreeInsertionPostConnectionStep::Script(handle));
} else if let Some(element) = node.as_element() {
if element.is_html_element("selectedcontent") {
steps.push(TreeInsertionPostConnectionStep::SelectedContent(handle));
} else if element.is_html_option() {
steps.push(TreeInsertionPostConnectionStep::SelectedOption(handle));
}
}
stack.extend(self.dom_host.child_handles_reversed(handle));
if let Some(shadow_root) = self.dom_host.shadow_root_handle(handle) {
stack.push(shadow_root);
}
}
steps
}
}
@@ -19,6 +19,8 @@ mod resources;
mod tree_order;
pub(in crate::document_runtime) use adoption::TreeAdoptionPlan;
pub(super) use insertion_plan::{TreeInsertionPlan, TreeInsertionPostConnectionStep};
pub(in crate::document_runtime) use parser_post_step::ParserPostStepRuntimeWork;
#[cfg(test)]
pub(crate) use parser_post_step::ParserPostStepRuntimeWorkForTest;
pub(super) use policy::TreeReactionDispatchPolicy;
@@ -155,6 +155,7 @@ impl DocumentRuntime {
if !changed {
return;
}
self.run_parser_tree_insertion_post_connection_steps(scope, host_ptr, &insertion_plan);
self.dispatch_tree_insertion_side_effects_after_change(
scope,
host_ptr,
@@ -5,7 +5,7 @@ pub(super) enum TreeMutationSideEffectSource {
}
#[derive(Clone, Copy)]
pub(super) enum TreeReactionDispatchPolicy {
pub(in crate::document_runtime::mutation_commands) enum TreeReactionDispatchPolicy {
DispatchNow,
AppendToCurrentQueue,
}
@@ -19,6 +19,7 @@ pub(super) struct TreeRemovalPlan {
pub(super) live_range_previous_sibling: Option<DomHandle>,
pub(super) node_iterator_plan: Option<NodeIteratorRemovalPlan>,
pub(super) registry_retargets: Vec<custom_elements::RegistryAssociationRetarget>,
pub(super) selected_option_owners_before_remove: Vec<(DomHandle, DomHandle)>,
}
impl DocumentRuntime {
@@ -58,6 +59,8 @@ impl DocumentRuntime {
};
let registry_retargets =
custom_elements::registry_association_retargets_before_removal(host_ptr, root);
let selected_option_owners_before_remove =
self.selected_option_owners_in_subtrees(std::slice::from_ref(&root));
TreeRemovalPlan {
parent,
root,
@@ -69,6 +72,7 @@ impl DocumentRuntime {
live_range_previous_sibling,
node_iterator_plan,
registry_retargets,
selected_option_owners_before_remove,
}
}
@@ -21,6 +21,11 @@ impl DocumentRuntime {
removal_plan: &TreeRemovalPlan,
profile: TreeMutationSourceProfile,
) {
self.queue_selectedcontent_updates_after_tree_removal(
scope,
host_ptr,
&removal_plan.selected_option_owners_before_remove,
);
self.dispatch_tree_removal_pre_reaction_followups_after_change(
scope,
host_ptr,
@@ -39,9 +39,11 @@ impl DocumentRuntime {
let _ = self
.remove_child_appending_to_current_reaction_queue(scope, host_ptr, parent, *child);
}
let changed = self
.append_child_appending_to_current_reaction_queue(scope, host_ptr, parent, fragment)
|| removes_existing_children;
let inserted_new_children = !added_children.is_empty()
&& self.append_child_appending_to_current_reaction_queue(
scope, host_ptr, parent, fragment,
);
let changed = inserted_new_children || removes_existing_children;
if changed && records_enabled {
crate::observer_runtime::coalesce_child_list_replacement_records(
host_ptr,
@@ -249,11 +251,14 @@ impl DocumentRuntime {
next_sibling,
);
}
let changed = self.apply_runtime_mutation_effects_with_prepublished_removals(
let changed = self.apply_tree_insertion_mutation_effects_with_post_connection_steps(
scope,
host_ptr,
&insertion_plan,
effects,
RuntimeMutationOptions::js_dom_api(),
reaction_policy,
true,
prepublished_removals,
);
if !changed {
@@ -279,6 +284,13 @@ impl DocumentRuntime {
replacement_plan: &TreeReplacementPlan<'_>,
reaction_policy: TreeReactionDispatchPolicy,
) {
self.queue_selectedcontent_updates_after_tree_removal(
scope,
host_ptr,
&replacement_plan
.removal
.selected_option_owners_before_remove,
);
let insertion_plan = &replacement_plan.insertion;
let mut dispatch_reactions = |scope: &mut v8::PinScope<'_, '_>| {
self.dispatch_tree_insertion_immediate_side_effects_after_change(
+32 -16
View File
@@ -113,11 +113,21 @@ impl RuntimeMutationOptions {
}
#[derive(Debug, Clone, Copy)]
struct ScriptStartRequest {
pub(super) struct ScriptStartRequest {
handle: NativeNodeId,
clears_force_async: bool,
}
impl ScriptStartRequest {
pub(super) fn handle(&self) -> NativeNodeId {
self.handle
}
pub(super) fn clears_force_async(&self) -> bool {
self.clears_force_async
}
}
#[derive(Debug, Default)]
struct ScriptStartRequests {
requests: Vec<ScriptStartRequest>,
@@ -244,20 +254,11 @@ impl MutationCoordinator {
}
let mutation_record_count = effects.observer_records().records().len();
let script_planning_started = cpu_profile_enabled.then(Instant::now);
let mut script_start_requests = ScriptStartRequests::default();
if prepare_connected_scripts {
for &root in effects.scripts().connected_roots() {
self.collect_connected_scripts_in_subtree(
dom_host,
root,
&mut script_start_requests,
);
}
for trigger in effects.scripts().prepare_triggers() {
script_start_requests.queue(trigger.handle(), trigger.clears_script_force_async());
}
}
let script_start_requests = script_start_requests.into_tree_order(dom_host);
let script_start_requests = if prepare_connected_scripts {
self.plan_connected_script_start_requests(dom_host, &effects)
} else {
Vec::new()
};
let script_start_request_count = script_start_requests.len();
let script_planning_us = script_planning_started
.map(|started| started.elapsed().as_micros())
@@ -376,7 +377,22 @@ impl MutationCoordinator {
}
}
fn collect_connected_script_start_candidate(
pub(super) fn plan_connected_script_start_requests(
&self,
dom_host: &mut DomHost,
effects: &DomMutationEffects,
) -> Vec<ScriptStartRequest> {
let mut requests = ScriptStartRequests::default();
for &root in effects.scripts().connected_roots() {
self.collect_connected_scripts_in_subtree(dom_host, root, &mut requests);
}
for trigger in effects.scripts().prepare_triggers() {
requests.queue(trigger.handle(), trigger.clears_script_force_async());
}
requests.into_tree_order(dom_host)
}
pub(super) fn collect_connected_script_start_candidate(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
@@ -302,6 +302,7 @@ impl JsContextHost {
super::misc_platform_api_tasks::MiscPlatformApiTaskState::default(),
file_entry_file_callbacks:
super::file_entry_file_callbacks::FileEntryFileCallbackState::default(),
pending_selectedcontent_updates: HashSet::new(),
user_interaction_tasks:
super::user_interaction_tasks::UserInteractionTaskState::default(),
pending_image_load_events: HashMap::new(),
@@ -1512,6 +1513,14 @@ impl JsContextHost {
}
}
pub(crate) fn mark_pending_selectedcontent_update(&mut self, select: DomHandle) -> bool {
self.pending_selectedcontent_updates.insert(select)
}
pub(crate) fn take_pending_selectedcontent_update(&mut self, select: DomHandle) -> bool {
self.pending_selectedcontent_updates.remove(&select)
}
pub(crate) fn pending_image_load_event(
&self,
handle: DomHandle,
@@ -909,6 +909,7 @@ pub(crate) struct JsContextHost {
directory_reader_callbacks: directory_reader_callbacks::DirectoryReaderCallbackState,
misc_platform_api_tasks: misc_platform_api_tasks::MiscPlatformApiTaskState,
file_entry_file_callbacks: file_entry_file_callbacks::FileEntryFileCallbackState,
pending_selectedcontent_updates: HashSet<DomHandle>,
user_interaction_tasks: user_interaction_tasks::UserInteractionTaskState,
pending_image_load_events: HashMap<DomHandle, PendingImageLoadEvent>,
next_image_load_event_id: u64,
@@ -636,6 +636,7 @@ pub(in crate::native_bridge) fn select_selected_index_setter_function<'s>(
let _ = runtime.set_selected_state(scope, runtime_ptr, option, should_select);
}
let _ = runtime.set_select_explicit_none(scope, runtime_ptr, handle, !matched);
let _ = runtime.sync_selectedcontents_for_select_in_reaction_scope(scope, runtime_ptr, handle);
rv.set_undefined();
}
@@ -400,6 +400,11 @@ pub(in crate::native_bridge) fn reset_form_default_action(
did_change |= runtime
.dom_host_mut()
.set_select_explicit_none_state(handle, false);
did_change |= runtime.sync_selectedcontents_for_select_in_reaction_scope(
scope,
runtime_ptr,
handle,
);
}
FormResetPlan::Output { handle, value } => {
did_change |= runtime.dom_host_mut().set_text_content(handle, &value);
+51 -5
View File
@@ -12400,7 +12400,7 @@ JSON.stringify({
}
#[test]
fn parser_option_finish_and_select_value_sync_selectedcontent_clones() {
fn parser_option_finish_and_select_setters_sync_selectedcontent_clones() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
@@ -12409,11 +12409,11 @@ JSON.stringify({
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">
<form id="form"><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>
</select></form>
<script>
const select = document.getElementById('select');
const selectedcontent = document.getElementById('selectedcontent');
@@ -12427,6 +12427,11 @@ window.selectedcontentState.push(
selectedcontent.textContent.trim(),
selectedcontent.firstElementChild.tagName,
);
document.querySelector('#two > strong').textContent = 'updated';
select.selectedIndex = select.selectedIndex;
window.selectedcontentState.push(selectedcontent.textContent.trim());
document.getElementById('form').reset();
window.selectedcontentState.push(selectedcontent.textContent.trim());
</script>
</body></html>"#,
)
@@ -12437,8 +12442,49 @@ window.selectedcontentState.push(
.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"
Some(r#"["one",true,"two","STRONG","updated","one"]"#),
"parser option completion and select setters must synchronously clone the selected option children"
);
}));
}
#[test]
fn moving_selected_option_updates_previous_select_selectedcontent_in_a_microtask() {
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="source">
<button><selectedcontent id="selectedcontent"></selectedcontent></button>
<option id="moved">one</option>
<option>two</option>
</select>
<div id="destination"></div>
<script>
const selectedcontent = document.getElementById('selectedcontent');
document.getElementById('destination').appendChild(document.getElementById('moved'));
window.selectedcontentMoveState = [selectedcontent.textContent.trim()];
</script>
</body></html>"#,
)
.await;
page_vm
.evaluate_expression("0")
.expect("selectedcontent removal microtask checkpoint should run");
let result = page_vm
.evaluate_expression(
"JSON.stringify([...window.selectedcontentMoveState, document.getElementById('selectedcontent').textContent.trim()])",
)
.expect("selectedcontent move state should evaluate");
assert_eq!(
result.get("value").and_then(serde_json::Value::as_str),
Some(r#"["one","two"]"#),
"implicit option removal must keep the old clone synchronously and update it at the next microtask checkpoint"
);
}));
}
+1 -1
View File
@@ -203,7 +203,7 @@ fn fill_credit_card_control(
};
if is_select {
let _ = unsafe { &mut *runtime_ptr }.set_select_value(handle, &value);
let _ = unsafe { &mut *runtime_ptr }.set_select_value(scope, runtime_ptr, handle, &value);
} else {
let _ = unsafe { &mut *runtime_ptr }.set_input_value(handle, &value);
}