fix(images): reselect picture requests after removal

This commit is contained in:
ldm0
2026-09-23 00:11:51 +08:00
parent 9c7ded3d73
commit ab1498da5a
7 changed files with 146 additions and 3 deletions
@@ -3061,7 +3061,6 @@ html/semantics/embedded-content/the-img-element/sizes/parse-a-sizes-attribute-qu
html/semantics/embedded-content/the-img-element/sizes/parse-a-sizes-attribute-standards-mode.html
html/semantics/embedded-content/the-img-element/sizes/parse-a-sizes-attribute-width-1000px.html
html/semantics/embedded-content/the-img-element/source-media-outside-doc.html
html/semantics/embedded-content/the-img-element/source-relevant-mutations.html
html/semantics/embedded-content/the-img-element/svg-img-density-vs-zoom.html
html/semantics/embedded-content/the-img-element/update-the-image-data/current-request-microtask-002.html
html/semantics/embedded-content/the-img-element/update-the-image-data/lazy-out-of-band-load.html
@@ -6168,6 +6168,7 @@ html/semantics/embedded-content/the-img-element/not-rendered-dimension-getter.ht
html/semantics/embedded-content/the-img-element/not-rendered-image-loading-lazy.html
html/semantics/embedded-content/the-img-element/null-image-source.html
html/semantics/embedded-content/the-img-element/remove-element-and-scroll.html
html/semantics/embedded-content/the-img-element/source-relevant-mutations.html
html/semantics/embedded-content/the-img-element/srcset-natural-size-in-dynamic-iframe.html
html/semantics/embedded-content/the-img-element/srcset/avoid-reload-on-resize.html
html/semantics/embedded-content/the-img-element/srcset/parse-a-srcset-attribute.html
@@ -1,5 +1,8 @@
use super::super::{dom_binding_timing_started, record_dom_binding_timing};
use super::{node_iterators::NodeIteratorRemovalPlan, policy::TreeMutationSourceProfile};
use super::{
node_iterators::NodeIteratorRemovalPlan, policy::TreeMutationSourceProfile,
resources::ImageRelevantMutationPlan,
};
use crate::{
custom_elements,
document_runtime::{DocumentRuntime, DomHandle},
@@ -20,6 +23,7 @@ pub(super) struct TreeRemovalPlan {
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)>,
pub(super) image_relevant_mutation_plan: ImageRelevantMutationPlan,
}
impl DocumentRuntime {
@@ -61,6 +65,8 @@ impl DocumentRuntime {
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));
let image_relevant_mutation_plan =
self.image_relevant_mutation_plan_before_remove(parent, root);
TreeRemovalPlan {
parent,
root,
@@ -73,6 +79,7 @@ impl DocumentRuntime {
node_iterator_plan,
registry_retargets,
selected_option_owners_before_remove,
image_relevant_mutation_plan,
}
}
@@ -45,6 +45,12 @@ impl DocumentRuntime {
TreeMutationSideEffectSource::ParserTreeSink => {}
}
self.dispatch_tree_removal_custom_element_reactions(scope, host_ptr, removal_plan, profile);
self.queue_image_relevant_mutation_loads(
scope,
host_ptr,
&removal_plan.image_relevant_mutation_plan,
profile.subresource_request_initiator_type(),
);
}
fn dispatch_tree_removal_pre_reaction_followups_after_change(
@@ -323,5 +323,11 @@ impl DocumentRuntime {
}
}
self.preserve_selectedness_for_insertion_plan(scope, host_ptr, insertion_plan);
self.queue_image_relevant_mutation_loads(
scope,
host_ptr,
&replacement_plan.removal.image_relevant_mutation_plan,
crate::types::SubresourceRequestInitiatorType::Script,
);
}
}
@@ -222,7 +222,32 @@ impl DocumentRuntime {
plan
}
fn queue_image_relevant_mutation_loads(
pub(super) fn image_relevant_mutation_plan_before_remove(
&self,
parent: DomHandle,
root: DomHandle,
) -> ImageRelevantMutationPlan {
if !self.dom_host.is_html_element_named(parent, "picture") {
return ImageRelevantMutationPlan::default();
}
if self.dom_host.is_html_element_named(root, "source") {
return ImageRelevantMutationPlan {
pictures: vec![parent],
images: Vec::new(),
};
}
ImageRelevantMutationPlan {
pictures: Vec::new(),
images: self
.dom_host
.is_html_element_named(root, "img")
.then_some(root)
.into_iter()
.collect(),
}
}
pub(super) fn queue_image_relevant_mutation_loads(
&self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
@@ -373,6 +373,105 @@ async fn image_current_src_is_empty_while_a_replacement_request_is_pending() {
);
}
#[tokio::test]
async fn picture_source_and_image_tree_mutations_reselect_requests() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
loader.set_image_fetch_enabled(true);
let (mut vm, _resource_completion_queue) =
new_storage_test_vm_with_loader_and_resource_completion_queue(
"https://picture-source-removal.test/page.html",
&loader,
);
vm.eval(
r#"
(() => {
if (!document.documentElement) {
document.append(document.createElement("html"));
}
if (!document.body) {
document.documentElement.append(document.createElement("body"));
}
const picture = document.createElement("picture");
const source1 = document.createElement("source");
const source2 = document.createElement("source");
const source3 = document.createElement("source");
const source4 = document.createElement("source");
const source5 = document.createElement("source");
const image1 = document.createElement("img");
const image2 = document.createElement("img");
source1.srcset = "data:,s1";
source2.srcset = "data:,s2";
source3.srcset = "data:,s3";
source4.srcset = "data:,s4";
source5.srcset = "data:,s5";
image1.src = "data:,img1";
image2.src = "data:,img2";
picture.append(source1, source2, source3, image1, image2, source4, source5);
const host = document.body;
host.append(picture);
globalThis.__pictureSourceMutation = {
host,
picture,
source1,
source2,
source3,
source4,
source5,
image1,
image2
};
})()
"#,
)
.expect("picture source mutation setup should evaluate");
let current_sources = |vm: &mut ScriptVm| {
vm.eval(
"[__pictureSourceMutation.image1.currentSrc, \
__pictureSourceMutation.image2.currentSrc].join(',')",
)
.expect("picture image current sources should evaluate")
};
assert_eq!(current_sources(&mut vm), "data:,s1,data:,s1");
vm.eval("__pictureSourceMutation.source1.remove()")
.expect("first picture source removal should evaluate");
assert_eq!(current_sources(&mut vm), "data:,s2,data:,s2");
vm.eval("__pictureSourceMutation.source2.remove()")
.expect("second picture source removal should evaluate");
assert_eq!(current_sources(&mut vm), "data:,s3,data:,s3");
vm.eval("__pictureSourceMutation.host.append(__pictureSourceMutation.source3)")
.expect("third picture source move should evaluate");
assert_eq!(current_sources(&mut vm), "data:,img1,data:,img2");
vm.eval("__pictureSourceMutation.source4.remove()")
.expect("trailing picture source removal should evaluate");
assert_eq!(current_sources(&mut vm), "data:,img1,data:,img2");
vm.eval("__pictureSourceMutation.host.append(__pictureSourceMutation.source5)")
.expect("trailing picture source move should evaluate");
assert_eq!(current_sources(&mut vm), "data:,img1,data:,img2");
vm.eval("__pictureSourceMutation.picture.prepend(__pictureSourceMutation.source2)")
.expect("second picture source reinsertion should evaluate");
assert_eq!(current_sources(&mut vm), "data:,s2,data:,s2");
vm.eval("__pictureSourceMutation.picture.prepend(__pictureSourceMutation.source1)")
.expect("first picture source reinsertion should evaluate");
assert_eq!(current_sources(&mut vm), "data:,s1,data:,s1");
vm.eval("__pictureSourceMutation.source1.replaceWith(document.createElement('div'))")
.expect("first picture source replacement should evaluate");
assert_eq!(current_sources(&mut vm), "data:,s2,data:,s2");
vm.eval("__pictureSourceMutation.image1.remove()")
.expect("first picture image removal should evaluate");
assert_eq!(current_sources(&mut vm), "data:,img1,data:,s2");
}
#[tokio::test]
async fn image_fetch_enabled_dispatches_http_error_after_resource_timing() {
let (base_url, request_rx, server) =