fix(images): track current request transitions

This commit is contained in:
ldm0
2026-09-23 00:11:51 +08:00
parent c95e6d49d0
commit 9c7ded3d73
14 changed files with 640 additions and 228 deletions
@@ -3052,7 +3052,6 @@ html/semantics/embedded-content/the-img-element/image-loading-lazy-in-scroller-n
html/semantics/embedded-content/the-img-element/image-loading-lazy-in-scroller-nested.html
html/semantics/embedded-content/the-img-element/image-loading-lazy-in-scroller.html
html/semantics/embedded-content/the-img-element/image-loading-lazy-in-viewport-dynamic.html
html/semantics/embedded-content/the-img-element/img-picture-ancestor.html
html/semantics/embedded-content/the-img-element/natural-size-orientation.html
html/semantics/embedded-content/the-img-element/naturalWidth-naturalHeight-width-height.html
html/semantics/embedded-content/the-img-element/relevant-mutations-lazy.html
@@ -3065,7 +3064,6 @@ 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/current-request-microtask.html
html/semantics/embedded-content/the-img-element/update-the-image-data/lazy-out-of-band-load.html
html/semantics/embedded-content/the-img-element/update-the-image-data/not-broken-while-load-task-scheduled.html
html/semantics/embedded-content/the-img-element/update-the-image-data/src-then-lazy-load.html
@@ -6155,6 +6155,7 @@ html/semantics/embedded-content/the-img-element/image-loading-lazy-subframe-deta
html/semantics/embedded-content/the-img-element/image-loading-lazy-to-eager.html
html/semantics/embedded-content/the-img-element/image-loading-lazy-use-list-of-available-images.html
html/semantics/embedded-content/the-img-element/img-created-in-active-document-crash.html
html/semantics/embedded-content/the-img-element/img-picture-ancestor.html
html/semantics/embedded-content/the-img-element/img-src-in-synthetic-document.html
html/semantics/embedded-content/the-img-element/img.complete.html
html/semantics/embedded-content/the-img-element/invalid-src.html
@@ -6174,6 +6175,7 @@ html/semantics/embedded-content/the-img-element/srcset/select-an-image-source.ht
html/semantics/embedded-content/the-img-element/srcset/srcset-media-dynamic.html
html/semantics/embedded-content/the-img-element/update-media.html
html/semantics/embedded-content/the-img-element/update-src-complete.html
html/semantics/embedded-content/the-img-element/update-the-image-data/current-request-microtask.html
html/semantics/embedded-content/the-img-element/update-the-image-data/fail-to-resolve.html
html/semantics/embedded-content/the-object-element/block-object-with-ruby-crash.html
html/semantics/embedded-content/the-object-element/historical.html
@@ -306,6 +306,7 @@ impl JsContextHost {
super::user_interaction_tasks::UserInteractionTaskState::default(),
pending_image_load_events: HashMap::new(),
next_image_load_event_id: 1,
current_image_requests: HashMap::new(),
pending_media_load_sequences: HashMap::new(),
next_media_load_sequence_id: 1,
pending_text_track_load_sequences: HashMap::new(),
@@ -1482,7 +1483,7 @@ impl JsContextHost {
&self,
handle: DomHandle,
) -> Option<super::PendingImageLoadEvent> {
self.pending_image_load_events.get(&handle).copied()
self.pending_image_load_events.get(&handle).cloned()
}
pub(crate) fn next_image_load_event_id(&mut self) -> super::ImageLoadEventId {
@@ -1625,6 +1626,24 @@ impl JsContextHost {
self.lazy_media_load_candidates.iter().copied().collect()
}
pub(crate) fn set_current_image_request(
&mut self,
handle: DomHandle,
request_key: Option<crate::types::ImageRequestKey>,
) {
if let Some(request_key) = request_key {
self.current_image_requests.insert(handle, request_key);
} else {
self.current_image_requests.remove(&handle);
}
}
pub(crate) fn current_image_request_url(&self, handle: DomHandle) -> Option<&str> {
self.current_image_requests
.get(&handle)
.map(crate::types::ImageRequestKey::url)
}
pub(crate) fn begin_form_data_construction(&mut self, form_handle: DomHandle) -> bool {
if self.constructing_form_data_forms.contains(&form_handle) {
return false;
@@ -105,9 +105,8 @@ impl JsContextHost {
self.pending_image_load_events
.iter()
.any(|(pending_element, pending)| {
self.pending_image_load_event_is_current(*pending_element, *pending)
&& image_selected_request_key(self, *pending_element).as_ref()
== Some(&request.request_key)
self.pending_image_load_event_is_current(*pending_element, pending)
&& pending.request_key() == Some(&request.request_key)
});
if waits_for_current_load {
request.state = PendingImageDecodeRequestState::PendingLoad;
@@ -343,5 +342,8 @@ fn image_source_can_decode(runtime: &JsContextHost, handle: DomHandle, source: &
{
return false;
}
runtime.image_resource_is_ready(handle) && image_intrinsic_dimensions(runtime, handle).is_some()
(runtime.image_resource_is_ready(handle)
&& image_intrinsic_dimensions(runtime, handle).is_some())
|| image_selected_request_key(runtime, handle)
.is_some_and(|request_key| runtime.has_ready_image_request(&request_key))
}
@@ -95,11 +95,10 @@ impl JsContextHost {
owner,
request_initiator_type,
);
if !self.insert_pending_image_load_event(image_handle, pending) {
if !self.insert_pending_image_load_event(image_handle, pending.clone()) {
let _ = self.settle_pending_image_load_event(pending, true);
return None;
}
self.begin_pending_image_resource(image_handle, pending);
match pending.owner() {
PendingImageLoadEventOwner::Main(binding) => tracing::debug!(
image = image_handle.index(),
@@ -123,7 +122,7 @@ impl JsContextHost {
pub(crate) fn pending_image_load_event_is_current(
&self,
image_handle: DomHandle,
pending: PendingImageLoadEvent,
pending: &PendingImageLoadEvent,
) -> bool {
if self.dom_host().owner_document_handle(image_handle)
!= Some(pending.owner_document_handle())
@@ -143,6 +142,34 @@ impl JsContextHost {
}
}
pub(crate) fn prepare_pending_image_load_request_if_matches(
&mut self,
image_handle: DomHandle,
id: ImageLoadEventId,
request_key: crate::types::ImageRequestKey,
) -> bool {
let pending = {
let Some(pending) = self.pending_image_load_events.get_mut(&image_handle) else {
return false;
};
if pending.id != id
|| pending.request_key.is_some()
|| pending.network_state != PendingImageLoadNetworkState::Unbound
{
return false;
}
pending.request_key = Some(request_key);
pending.clone()
};
self.begin_pending_image_resource(image_handle, &pending);
tracing::debug!(
image = image_handle.index(),
sequence = id.get(),
"selected image request identity"
);
true
}
pub(crate) fn bind_pending_image_load_network_request_if_matches(
&mut self,
image_handle: DomHandle,
@@ -152,10 +179,14 @@ impl JsContextHost {
let Some(pending) = self.pending_image_load_events.get_mut(&image_handle) else {
return false;
};
if pending.id != id || pending.network_state != PendingImageLoadNetworkState::Unbound {
if pending.id != id
|| pending.request_key.is_none()
|| pending.network_state != PendingImageLoadNetworkState::Unbound
{
return false;
}
pending.network_state = PendingImageLoadNetworkState::Pending(internal_id);
self.set_current_image_request(image_handle, None);
tracing::debug!(
image = image_handle.index(),
sequence = id.get(),
@@ -171,24 +202,32 @@ impl JsContextHost {
id: ImageLoadEventId,
successful: bool,
) -> Option<RendererPageImageLoadEventKind> {
let pending = self.pending_image_load_events.get_mut(&image_handle)?;
if pending.id != id || pending.network_state != PendingImageLoadNetworkState::Unbound {
return None;
}
self.pending_image_load_events
.get(&image_handle)
.filter(|pending| {
pending.id == id && pending.network_state == PendingImageLoadNetworkState::Unbound
})?;
let _ = self.retire_image_resource_for_element(image_handle);
let pending = self.pending_image_load_events.get_mut(&image_handle)?;
pending.network_state = if successful {
PendingImageLoadNetworkState::Ready(PendingImageLoadTerminalSource::Local)
} else {
PendingImageLoadNetworkState::Failed(PendingImageLoadTerminalSource::Local)
let (followup, current_request) = {
let pending = self.pending_image_load_events.get_mut(&image_handle)?;
pending.network_state = if successful {
PendingImageLoadNetworkState::Ready(PendingImageLoadTerminalSource::Local)
} else {
PendingImageLoadNetworkState::Failed(PendingImageLoadTerminalSource::Local)
};
(
pending_image_load_terminal_followup(pending),
pending.request_key.clone(),
)
};
self.set_current_image_request(image_handle, current_request);
tracing::debug!(
image = image_handle.index(),
sequence = id.get(),
successful,
"completed local or policy image resource"
);
pending_image_load_terminal_followup(pending)
followup
}
pub(crate) fn complete_pending_image_load_reused_resource_if_matches(
@@ -199,18 +238,25 @@ impl JsContextHost {
if !self.image_resource_is_ready(image_handle) {
return None;
}
let pending = self.pending_image_load_events.get_mut(&image_handle)?;
if pending.id != id || pending.network_state != PendingImageLoadNetworkState::Unbound {
return None;
}
pending.network_state =
PendingImageLoadNetworkState::Ready(PendingImageLoadTerminalSource::Local);
let (followup, current_request) = {
let pending = self.pending_image_load_events.get_mut(&image_handle)?;
if pending.id != id || pending.network_state != PendingImageLoadNetworkState::Unbound {
return None;
}
pending.network_state =
PendingImageLoadNetworkState::Ready(PendingImageLoadTerminalSource::Local);
(
pending_image_load_terminal_followup(pending),
pending.request_key.clone(),
)
};
self.set_current_image_request(image_handle, current_request);
tracing::debug!(
image = image_handle.index(),
sequence = id.get(),
"reused active decoded image resource without another fetch or decode"
);
pending_image_load_terminal_followup(pending)
followup
}
pub(crate) fn complete_pending_image_load_network_request_if_matches(
@@ -220,9 +266,13 @@ impl JsContextHost {
internal_id: u64,
successful: bool,
) -> Option<RendererPageImageLoadEventKind> {
let pending = self.pending_image_load_events.get_mut(&image_handle)?;
if pending.id != id
|| pending.network_state != PendingImageLoadNetworkState::Pending(internal_id)
if !self
.pending_image_load_events
.get(&image_handle)
.is_some_and(|pending| {
pending.id == id
&& pending.network_state == PendingImageLoadNetworkState::Pending(internal_id)
})
{
tracing::debug!(
image = image_handle.index(),
@@ -234,12 +284,19 @@ impl JsContextHost {
return None;
}
let _ = self.retire_image_resource_for_element(image_handle);
let pending = self.pending_image_load_events.get_mut(&image_handle)?;
pending.network_state = if successful {
PendingImageLoadNetworkState::Ready(PendingImageLoadTerminalSource::Network)
} else {
PendingImageLoadNetworkState::Failed(PendingImageLoadTerminalSource::Network)
let (followup, current_request) = {
let pending = self.pending_image_load_events.get_mut(&image_handle)?;
pending.network_state = if successful {
PendingImageLoadNetworkState::Ready(PendingImageLoadTerminalSource::Network)
} else {
PendingImageLoadNetworkState::Failed(PendingImageLoadTerminalSource::Network)
};
(
pending_image_load_terminal_followup(pending),
pending.request_key.clone(),
)
};
self.set_current_image_request(image_handle, current_request);
tracing::debug!(
image = image_handle.index(),
sequence = id.get(),
@@ -247,7 +304,7 @@ impl JsContextHost {
successful,
"applied image network terminal to lifecycle sequence"
);
pending_image_load_terminal_followup(pending)
followup
}
pub(crate) fn pending_image_load_terminal_followup_if_ready(
@@ -269,7 +326,7 @@ impl JsContextHost {
let pending = self
.pending_image_load_event(task_id.element())
.filter(|pending| pending.id() == task_id.sequence())?;
if !self.pending_image_load_event_is_current(task_id.element(), pending) {
if !self.pending_image_load_event_is_current(task_id.element(), &pending) {
return None;
}
let kind = pending
@@ -90,7 +90,7 @@ impl ImageResponseCompletion {
}
impl super::PendingImageLoadEvent {
fn document_task_owner(self) -> FrameDocumentTaskOwner {
fn document_task_owner(&self) -> FrameDocumentTaskOwner {
match self.owner() {
super::PendingImageLoadEventOwner::Main(binding) => binding.owner(),
super::PendingImageLoadEventOwner::Child(binding) => binding.owner(),
@@ -113,11 +113,9 @@ impl super::JsContextHost {
pub(super) fn begin_pending_image_resource(
&mut self,
element: DomHandle,
pending: super::PendingImageLoadEvent,
pending: &super::PendingImageLoadEvent,
) {
let Some(request_key) =
crate::native_bridge::element::image_selected_request_key(self, element)
else {
let Some(request_key) = pending.request_key().cloned() else {
self.image_resources.retire_element(element);
return;
};
@@ -333,14 +331,16 @@ impl super::JsContextHost {
descriptor: Option<ImageResponseDescriptor>,
encoded: ParkableImage,
) -> ImageResponseCompletion {
self.complete_pending_image_response_if_matches(
let completion = self.complete_pending_image_response_if_matches(
element,
sequence,
None,
super::PendingImageLoadTerminalSource::Local,
descriptor,
encoded,
)
);
self.update_current_image_request_after_response(element, completion);
completion
}
pub(crate) fn complete_pending_image_load_network_response_if_matches(
@@ -351,14 +351,31 @@ impl super::JsContextHost {
descriptor: Option<ImageResponseDescriptor>,
encoded: ParkableImage,
) -> ImageResponseCompletion {
self.complete_pending_image_response_if_matches(
let completion = self.complete_pending_image_response_if_matches(
element,
sequence,
Some(internal_id),
super::PendingImageLoadTerminalSource::Network,
descriptor,
encoded,
)
);
self.update_current_image_request_after_response(element, completion);
completion
}
fn update_current_image_request_after_response(
&mut self,
element: DomHandle,
completion: ImageResponseCompletion,
) {
if !completion.accepted() {
return;
}
let request_key = self
.pending_image_load_events
.get(&element)
.and_then(|pending| pending.request_key.clone());
self.set_current_image_request(element, request_key);
}
fn complete_pending_image_response_if_matches(
@@ -571,18 +588,22 @@ impl super::JsContextHost {
false
}
};
let Some(pending) = self.pending_image_load_events.get_mut(&task_id.element()) else {
return false;
let request_key = {
let Some(pending) = self.pending_image_load_events.get_mut(&task_id.element()) else {
return false;
};
if pending.id != task_id.sequence() {
return false;
}
pending.network_state = if successful {
super::PendingImageLoadNetworkState::Ready(source)
} else {
super::PendingImageLoadNetworkState::Failed(source)
};
pending.terminal_followup_queued = true;
pending.request_key.clone()
};
if pending.id != task_id.sequence() {
return false;
}
pending.network_state = if successful {
super::PendingImageLoadNetworkState::Ready(source)
} else {
super::PendingImageLoadNetworkState::Failed(source)
};
pending.terminal_followup_queued = true;
self.set_current_image_request(task_id.element(), request_key);
true
}
}
@@ -688,13 +688,14 @@ pub(crate) enum PendingImageLoadEventOwner {
Child(FrameDocumentImageLoadEventBinding),
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub(crate) struct PendingImageLoadEvent {
id: ImageLoadEventId,
owner_document_handle: DomHandle,
target: WindowDocumentTaskTarget,
owner: PendingImageLoadEventOwner,
request_initiator_type: crate::types::SubresourceRequestInitiatorType,
request_key: Option<ImageRequestKey>,
network_state: PendingImageLoadNetworkState,
terminal_followup_queued: bool,
}
@@ -728,32 +729,37 @@ impl PendingImageLoadEvent {
target,
owner,
request_initiator_type,
request_key: None,
network_state: PendingImageLoadNetworkState::Unbound,
terminal_followup_queued: false,
}
}
pub(crate) fn id(self) -> ImageLoadEventId {
pub(crate) fn id(&self) -> ImageLoadEventId {
self.id
}
pub(crate) fn owner_document_handle(self) -> DomHandle {
pub(crate) fn owner_document_handle(&self) -> DomHandle {
self.owner_document_handle
}
pub(crate) fn target(self) -> WindowDocumentTaskTarget {
pub(crate) fn target(&self) -> WindowDocumentTaskTarget {
self.target
}
pub(crate) fn owner(self) -> PendingImageLoadEventOwner {
pub(crate) fn owner(&self) -> PendingImageLoadEventOwner {
self.owner
}
pub(crate) fn request_initiator_type(self) -> crate::types::SubresourceRequestInitiatorType {
pub(crate) fn request_initiator_type(&self) -> crate::types::SubresourceRequestInitiatorType {
self.request_initiator_type
}
pub(crate) fn network_request_id(self) -> Option<u64> {
pub(crate) fn request_key(&self) -> Option<&ImageRequestKey> {
self.request_key.as_ref()
}
pub(crate) fn network_request_id(&self) -> Option<u64> {
match self.network_state {
PendingImageLoadNetworkState::Pending(internal_id) => Some(internal_id),
PendingImageLoadNetworkState::Unbound
@@ -763,7 +769,7 @@ impl PendingImageLoadEvent {
}
}
pub(crate) fn terminal_source(self) -> Option<PendingImageLoadTerminalSource> {
pub(crate) fn terminal_source(&self) -> Option<PendingImageLoadTerminalSource> {
match self.network_state {
PendingImageLoadNetworkState::Ready(source)
| PendingImageLoadNetworkState::Failed(source) => Some(source),
@@ -774,7 +780,7 @@ impl PendingImageLoadEvent {
}
pub(crate) fn terminal_followup(
self,
&self,
) -> Option<crate::page_task_queue::RendererPageImageLoadEventKind> {
if !self.terminal_followup_queued {
return None;
@@ -912,6 +918,9 @@ pub(crate) struct JsContextHost {
user_interaction_tasks: user_interaction_tasks::UserInteractionTaskState,
pending_image_load_events: HashMap<DomHandle, PendingImageLoadEvent>,
next_image_load_event_id: u64,
// Source selection can change before its pending request becomes available or broken. Keep
// the request exposed by currentSrc separate from the element's mutable source attributes.
current_image_requests: HashMap<DomHandle, ImageRequestKey>,
pending_media_load_sequences: HashMap<DomHandle, PendingMediaLoadSequence>,
next_media_load_sequence_id: u64,
pending_text_track_load_sequences: HashMap<DomHandle, PendingTextTrackLoadSequence>,
@@ -6,7 +6,7 @@ use crate::page_task_queue::{
RendererPageImageLoadEventTaskId,
};
use crate::types::{ImageRequestCorsMode, ImageRequestKey, SubresourceRequestInitiatorType};
use crate::util::v8_string;
use crate::util::{context_host_ptr_from_global_bridge, v8_string};
use moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE;
use crate::context_bootstrap::evaluate_match_media_query_list_with_viewport;
@@ -16,11 +16,15 @@ use super::super::geometry::compute_mock_client_rect;
use super::super::{construct_simple_event, dispatch_public_event, resolve_url_like_attribute};
use super::lazy::{image_load_is_deferred, revealed_lazy_image_handles};
#[derive(Clone, Copy)]
const IMAGE_UPDATE_HANDLE_INDEX: u32 = 0;
const IMAGE_UPDATE_ID_INDEX: u32 = 1;
const IMAGE_UPDATE_TRIGGER_INDEX: u32 = 2;
#[derive(Clone, Copy, Eq, PartialEq)]
#[repr(u32)]
enum ImageLoadQueueTrigger {
SourceOrInsertion,
LazyReveal,
DocumentAdoption,
}
pub(crate) fn queue_image_load_event_if_needed(
@@ -46,7 +50,7 @@ pub(crate) fn queue_image_load_event_after_document_adoption(
scope,
runtime_ptr,
handle,
ImageLoadQueueTrigger::DocumentAdoption,
ImageLoadQueueTrigger::SourceOrInsertion,
SubresourceRequestInitiatorType::Other,
);
}
@@ -99,7 +103,7 @@ pub(crate) fn queue_image_load_event_for_loading_change(
scope,
runtime_ptr,
handle,
ImageLoadQueueTrigger::SourceOrInsertion,
ImageLoadQueueTrigger::LazyReveal,
SubresourceRequestInitiatorType::Script,
);
}
@@ -116,15 +120,9 @@ fn queue_image_load_event(
if image_load_is_deferred(runtime, handle) && !allow_deferred_lazy {
return;
}
if image_load_is_suppressed_for_non_active_parent(runtime, handle)
&& !matches!(trigger, ImageLoadQueueTrigger::DocumentAdoption)
if image_selected_source(runtime, handle).is_none()
&& !connected_empty_src_without_srcset(runtime, handle)
{
return;
}
let selected_source = image_selected_source_url(runtime, handle);
let invalid_empty_source =
selected_source.is_empty() && connected_empty_src_without_srcset(runtime, handle);
if selected_source.is_empty() && !invalid_empty_source {
let _ = runtime.cancel_pending_image_load_event(handle);
let _ = runtime.process_image_decode_requests_for_element(scope, handle);
return;
@@ -149,7 +147,7 @@ fn queue_image_load_event(
if let Some(pending) = runtime
.pending_image_load_event(handle)
.or(pending_before_registration)
.filter(|pending| runtime.pending_image_load_event_is_current(handle, *pending))
.filter(|pending| runtime.pending_image_load_event_is_current(handle, pending))
{
let followup =
runtime.pending_image_load_terminal_followup_if_ready(handle, pending.id());
@@ -163,71 +161,134 @@ fn queue_image_load_event(
let _ = runtime.process_image_decode_requests_for_element(scope, handle);
return;
};
// Register the current request before reconsidering decode() promises. An
// image may receive src and decode() while detached, then be inserted in
// the same script job. Processing the promise before this registration
// would reject it even though insertion has just started its exact request.
let _ = runtime.process_image_decode_requests_for_element(scope, handle);
queue_image_update_microtask(scope, runtime_ptr, handle, pending.id(), trigger);
}
fn queue_image_update_microtask(
scope: &mut v8::PinScope<'_, '_>,
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
sequence: ImageLoadEventId,
trigger: ImageLoadQueueTrigger,
) {
let data = v8::Array::new(scope, 3);
let _ = data.set_index(
scope,
IMAGE_UPDATE_HANDLE_INDEX,
v8::BigInt::new_from_u64(scope, handle.index() as u64).into(),
);
let _ = data.set_index(
scope,
IMAGE_UPDATE_ID_INDEX,
v8::BigInt::new_from_u64(scope, sequence.get()).into(),
);
let _ = data.set_index(
scope,
IMAGE_UPDATE_TRIGGER_INDEX,
v8::Integer::new_from_unsigned(scope, trigger as u32).into(),
);
let Some(callback) = v8::Function::builder(queued_image_update_microtask_callback)
.data(data.into())
.build(scope)
else {
let _ = unsafe { &mut *runtime_ptr }
.cancel_pending_image_load_event_if_matches(handle, sequence);
return;
};
scope.enqueue_microtask(callback);
}
fn queued_image_update_microtask_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(runtime_ptr) = context_host_ptr_from_global_bridge(scope) else {
return;
};
let Some((handle, sequence, trigger)) =
image_update_target_from_callback_data(scope, args.data())
else {
return;
};
let runtime = unsafe { &mut *runtime_ptr };
let Some(pending) = runtime
.pending_image_load_event(handle)
.filter(|pending| pending.id() == sequence)
else {
return;
};
if !runtime.pending_image_load_event_is_current(handle, &pending) {
let _ = runtime.cancel_pending_image_load_event_if_matches(handle, sequence);
return;
}
if pending.request_key().is_some() {
return;
}
if image_load_is_deferred(runtime, handle) && trigger != ImageLoadQueueTrigger::LazyReveal {
let _ = runtime.cancel_pending_image_load_event_if_matches(handle, sequence);
return;
}
let Some(request_key) = image_selected_request_key(runtime, handle) else {
if connected_empty_src_without_srcset(runtime, handle) {
let followup = runtime
.complete_pending_image_load_local_resource_if_matches(handle, sequence, false);
queue_image_load_terminal_followup_if_ready(runtime_ptr, handle, sequence, followup);
} else {
let _ = runtime.cancel_pending_image_load_event_if_matches(handle, sequence);
}
let _ = runtime.process_image_decode_requests_for_element(scope, handle);
return;
};
if !runtime.prepare_pending_image_load_request_if_matches(handle, sequence, request_key.clone())
{
return;
}
runtime.set_current_image_request(handle, Some(request_key.clone()));
if runtime.image_resource_is_ready(handle)
&& !runtime.has_scanned_image_preload_for_element(handle)
{
let followup =
runtime.complete_pending_image_load_reused_resource_if_matches(handle, pending.id());
queue_image_load_terminal_followup_if_ready(runtime_ptr, handle, pending.id(), followup);
runtime.complete_pending_image_load_reused_resource_if_matches(handle, sequence);
queue_image_load_terminal_followup_if_ready(runtime_ptr, handle, sequence, followup);
let _ = runtime.process_image_decode_requests_for_element(scope, handle);
return;
}
let start = if invalid_empty_source {
Err("an empty image source cannot be fetched".to_owned())
} else {
url::Url::parse(&selected_source)
.map_err(|error| error.to_string())
.and_then(|request_url| {
if runtime
.check_top_document_subresource_csp(
scope,
&request_url,
DocumentSubresourceCspKind::Image,
)
.blocks_request()
{
return Ok(crate::network_host::ImageElementResourceFetchStart::Failed);
}
crate::network_host::start_image_element_resource_fetch(
let start = url::Url::parse(request_key.url())
.map_err(|error| error.to_string())
.and_then(|request_url| {
if runtime
.check_top_document_subresource_csp(
scope,
runtime,
handle,
pending.id(),
request_url,
&request_url,
DocumentSubresourceCspKind::Image,
)
})
};
.blocks_request()
{
return Ok(crate::network_host::ImageElementResourceFetchStart::Failed);
}
crate::network_host::start_image_element_resource_fetch(
scope,
runtime,
handle,
sequence,
request_url,
)
});
match start {
Ok(crate::network_host::ImageElementResourceFetchStart::Pending) => {}
Ok(crate::network_host::ImageElementResourceFetchStart::Failed) => {
let followup = runtime.complete_pending_image_load_local_resource_if_matches(
handle,
pending.id(),
false,
);
queue_image_load_terminal_followup_if_ready(
runtime_ptr,
handle,
pending.id(),
followup,
);
let followup = runtime
.complete_pending_image_load_local_resource_if_matches(handle, sequence, false);
queue_image_load_terminal_followup_if_ready(runtime_ptr, handle, sequence, followup);
}
Ok(crate::network_host::ImageElementResourceFetchStart::PolicySkipped) => {
let followup = runtime.complete_pending_image_load_local_resource_if_matches(
handle,
pending.id(),
true,
);
queue_image_load_terminal_followup_if_ready(
runtime_ptr,
handle,
pending.id(),
followup,
);
let followup = runtime
.complete_pending_image_load_local_resource_if_matches(handle, sequence, true);
queue_image_load_terminal_followup_if_ready(runtime_ptr, handle, sequence, followup);
}
Ok(crate::network_host::ImageElementResourceFetchStart::Local {
descriptor,
@@ -235,39 +296,63 @@ fn queue_image_load_event(
}) => {
let completion = runtime.complete_pending_image_load_local_response_if_matches(
handle,
pending.id(),
sequence,
descriptor,
encoded,
);
queue_image_load_terminal_followup_if_ready(
runtime_ptr,
handle,
pending.id(),
sequence,
completion.followup(),
);
}
Err(error) => {
tracing::debug!(
image = handle.index(),
sequence = pending.id().get(),
sequence = sequence.get(),
%error,
"image resource selection failed"
);
let followup = runtime.complete_pending_image_load_local_resource_if_matches(
handle,
pending.id(),
false,
);
queue_image_load_terminal_followup_if_ready(
runtime_ptr,
handle,
pending.id(),
followup,
);
let followup = runtime
.complete_pending_image_load_local_resource_if_matches(handle, sequence, false);
queue_image_load_terminal_followup_if_ready(runtime_ptr, handle, sequence, followup);
}
}
}
fn image_update_target_from_callback_data(
scope: &mut v8::PinScope<'_, '_>,
data: v8::Local<'_, v8::Value>,
) -> Option<(DomHandle, ImageLoadEventId, ImageLoadQueueTrigger)> {
let data = v8::Local::<v8::Array>::try_from(data).ok()?;
let handle = data.get_index(scope, IMAGE_UPDATE_HANDLE_INDEX)?;
let handle = v8::Local::<v8::BigInt>::try_from(handle).ok()?;
let (handle, handle_lossless) = handle.u64_value();
let sequence = data.get_index(scope, IMAGE_UPDATE_ID_INDEX)?;
let sequence = v8::Local::<v8::BigInt>::try_from(sequence).ok()?;
let (sequence, sequence_lossless) = sequence.u64_value();
let trigger = data
.get_index(scope, IMAGE_UPDATE_TRIGGER_INDEX)?
.uint32_value(scope)
.and_then(|value| match value {
value if value == ImageLoadQueueTrigger::SourceOrInsertion as u32 => {
Some(ImageLoadQueueTrigger::SourceOrInsertion)
}
value if value == ImageLoadQueueTrigger::LazyReveal as u32 => {
Some(ImageLoadQueueTrigger::LazyReveal)
}
_ => None,
})?;
(handle_lossless && sequence_lossless).then(|| {
(
DomHandle::new(handle as usize),
ImageLoadEventId::new(sequence),
trigger,
)
})
}
pub(crate) fn image_selected_source(runtime: &JsContextHost, handle: DomHandle) -> Option<String> {
image_selected_source_candidate(runtime, handle).map(|candidate| candidate.url)
}
@@ -369,22 +454,6 @@ fn image_selected_picture_source(
None
}
fn image_load_is_suppressed_for_non_active_parent(
runtime: &JsContextHost,
handle: DomHandle,
) -> bool {
runtime
.dom_host()
.node(handle)
.is_some_and(|node| !node.is_connected() && node.parent_node().is_some())
}
pub(crate) fn image_selected_source_url(runtime: &JsContextHost, handle: DomHandle) -> String {
image_selected_resource(runtime, handle)
.map(|selected| selected.url)
.unwrap_or_default()
}
fn image_selected_resource(
runtime: &JsContextHost,
handle: DomHandle,
@@ -523,8 +592,21 @@ pub(crate) fn apply_image_attribute_mutation_plan(
}
pub(crate) fn reset_image_load_dispatch(runtime: &mut JsContextHost, handle: DomHandle) {
let selected_request = image_selected_request_key(runtime, handle);
let selected_request_is_ready = selected_request
.as_ref()
.is_some_and(|request_key| runtime.has_ready_image_request(request_key));
let _ = runtime.cancel_pending_image_load_event(handle);
let _ = runtime.retire_image_resource_for_element(handle);
match selected_request {
None => {
runtime.set_current_image_request(handle, None);
let _ = runtime.retire_image_resource_for_element(handle);
}
Some(request_key) if selected_request_is_ready => {
runtime.set_current_image_request(handle, Some(request_key));
}
Some(_) => {}
}
if let Some(element) = runtime
.dom_host_mut()
.node_mut(handle)
@@ -544,19 +626,19 @@ pub(crate) fn mark_image_load_dispatched(runtime: &mut JsContextHost, handle: Do
}
}
pub(crate) fn record_image_resource_performance_entry_for_handle(
fn record_image_resource_performance_entry_for_pending(
scope: &mut v8::PinScope<'_, '_>,
runtime: &JsContextHost,
handle: DomHandle,
pending: &crate::native_bridge::PendingImageLoadEvent,
) {
let url = image_selected_source_url(runtime, handle);
if url.is_empty() {
let Some(request_key) = pending.request_key() else {
return;
}
};
crate::context_bootstrap::record_resource_performance_entry(
scope,
crate::context_bootstrap::ResourcePerformanceEntry::without_network_result(
url, "img", None,
request_key.url(),
"img",
None,
),
);
}
@@ -588,7 +670,7 @@ pub(crate) fn apply_authorized_image_load_event_in_context(
}
let pending =
runtime.take_pending_image_load_event_task_for_exact_target(task_id, target, kind)?;
let dispatched = dispatch_queued_image_load_event(scope, runtime_ptr, handle, pending, kind);
let dispatched = dispatch_queued_image_load_event(scope, runtime_ptr, handle, &pending, kind);
let _ = runtime.settle_pending_image_load_event(pending, true);
Some(if dispatched {
PageImageLoadEventTargetEffect::DispatchedToCurrentOwner
@@ -601,7 +683,7 @@ fn dispatch_queued_image_load_event(
scope: &mut v8::PinScope<'_, '_>,
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
pending: crate::native_bridge::PendingImageLoadEvent,
pending: &crate::native_bridge::PendingImageLoadEvent,
kind: RendererPageImageLoadEventKind,
) -> bool {
let runtime = unsafe { &mut *runtime_ptr };
@@ -613,7 +695,7 @@ fn dispatch_queued_image_load_event(
&& pending.terminal_source()
== Some(crate::native_bridge::PendingImageLoadTerminalSource::Local)
{
record_image_resource_performance_entry_for_handle(scope, runtime, handle);
record_image_resource_performance_entry_for_pending(scope, pending);
}
let _ = runtime.process_pending_image_decode_requests(scope);
let already_dispatched = runtime
@@ -1240,17 +1322,10 @@ fn image_current_src_value<'s>(
}
fn image_current_src_value_for_handle(runtime: &JsContextHost, handle: DomHandle) -> String {
let complete = runtime
.dom_host()
.node(handle)
.and_then(Node::as_element)
.is_some_and(|element| element.image_load_dispatched())
|| runtime.image_resource_is_ready(handle);
if complete {
image_selected_source_url(runtime, handle)
} else {
String::new()
}
runtime
.current_image_request_url(handle)
.unwrap_or_default()
.to_owned()
}
#[cfg(test)]
+7 -1
View File
@@ -169,9 +169,15 @@ pub(crate) fn start_image_element_resource_fetch(
.pending_image_load_event(image_handle)
.filter(|pending| pending.id() == sequence)
.ok_or_else(|| "image lifecycle sequence is no longer pending".to_owned())?;
if !host.pending_image_load_event_is_current(image_handle, pending) {
if !host.pending_image_load_event_is_current(image_handle, &pending) {
return Err("image lifecycle sequence owner is stale".to_owned());
}
if pending
.request_key()
.is_none_or(|request_key| request_key.url() != request_url.as_str())
{
return Err("image lifecycle request identity changed before fetch".to_owned());
}
let owner = match pending.owner() {
PendingImageLoadEventOwner::Main(binding)
if host.main_document_task_owner_is_current(binding.owner()) =>
@@ -187,7 +187,7 @@ image.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABA
}
#[tokio::test(flavor = "current_thread")]
async fn image_event_discards_a_document_open_task_before_applying_the_replacement_tail() {
async fn image_update_discards_a_retired_document_request_before_queuing_its_terminal_task() {
run_page_vm_async_test(async move {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
@@ -232,28 +232,8 @@ currentImage.src = "/current-without-network.png";
"image event dispatch must not acquire a PageTimer descriptor"
);
let stale_task = take_next_image_load_event_task_for_test(&mut page_vm)
.expect("retired image task should settle as one explicit turn");
let stale = page_vm.apply_selected_page_image_load_event_turn(stale_task)?;
let stale_action = stale.action;
assert_eq!(
stale_action.target_effect,
PageImageLoadEventTargetEffect::DiscardedStaleOwner {
current_owner: None,
stale_payload_effect: PageImageLoadEventStalePayloadEffect::NoSettledExactPayload,
}
);
assert_eq!(
page_vm
.vm_mut()
.eval("__documentExactImageEvents.join('|')")?,
""
);
let current = take_next_image_load_event_task_for_test(&mut page_vm)
.expect("replacement image task should survive stale-head settlement");
assert_ne!(stale_action.owner.target(), current.owner().target());
.expect("replacement image task should survive retired request cancellation");
page_vm
.run_claimed_dom_manipulation_task_through_selected_dispatcher_for_test(
crate::page_task_queue::RendererPageDomManipulationTask::ImageLoadEvent(current),
@@ -268,7 +248,7 @@ currentImage.src = "/current-without-network.png";
);
assert!(
take_next_image_load_event_task_for_test(&mut page_vm).is_none(),
"both exact-Document tasks must consume exactly two turns"
"the retired request must be cancelled before it manufactures a terminal task"
);
Ok::<_, anyhow::Error>(())
})
+24 -10
View File
@@ -13977,16 +13977,23 @@ window.parserMixedAdoptEvents.length = 0;
None,
"parser DocumentFragment insertion must not represent text-track default-mode work as a timer"
);
assert!(matches!(
take_next_dom_manipulation_task_for_test(&page_vm),
crate::page_task_queue::RendererPageDomManipulationTask::ImageLoadEvent(_)
));
page_vm
.vm_mut()
.perform_script_task_checkpoint(None)
.expect("parser task-end checkpoint should select the image request");
assert!(
matches!(
take_next_dom_manipulation_task_for_test(&page_vm),
crate::page_task_queue::RendererPageDomManipulationTask::TextTrackDefaultMode(_)
),
"hoisted text track should follow the earlier image in the shared DOM FIFO"
"the synchronous text-track task should precede the image terminal queued by the task-end microtask"
);
assert!(
matches!(
take_next_dom_manipulation_task_for_test(&page_vm),
crate::page_task_queue::RendererPageDomManipulationTask::ImageLoadEvent(_)
),
"the image-update microtask should queue its terminal after the earlier text-track task"
);
}));
}
@@ -14094,16 +14101,23 @@ window.parserMixedAdoptEvents.length = 0;
None,
"parser DocumentFragment insertBefore must not represent text-track default-mode work as a timer"
);
assert!(matches!(
take_next_dom_manipulation_task_for_test(&page_vm),
crate::page_task_queue::RendererPageDomManipulationTask::ImageLoadEvent(_)
));
page_vm
.vm_mut()
.perform_script_task_checkpoint(None)
.expect("parser task-end checkpoint should select the image request");
assert!(
matches!(
take_next_dom_manipulation_task_for_test(&page_vm),
crate::page_task_queue::RendererPageDomManipulationTask::TextTrackDefaultMode(_)
),
"insertBefore-hoisted text track should follow the image in the shared DOM FIFO"
"the synchronous text-track task should precede the image terminal queued by the task-end microtask"
);
assert!(
matches!(
take_next_dom_manipulation_task_for_test(&page_vm),
crate::page_task_queue::RendererPageDomManipulationTask::ImageLoadEvent(_)
),
"the image-update microtask should queue its terminal after the earlier text-track task"
);
}));
}
@@ -262,6 +262,117 @@ async fn image_fetch_enabled_discards_stale_queued_terminal_event() {
);
}
#[tokio::test]
async fn image_current_src_switches_only_when_the_update_microtask_selects_a_current_request() {
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://image-current-request.test/page.html",
&loader,
);
let before_update = vm
.eval(
r#"
(() => {
const picture = document.createElement("picture");
const nonMatchingSource = document.createElement("source");
nonMatchingSource.media = "not all";
nonMatchingSource.srcset = "data:,a";
picture.append(nonMatchingSource);
const matchingSource = document.createElement("source");
matchingSource.media = "all";
matchingSource.srcset = "data:,b";
picture.append(matchingSource);
const image = document.createElement("img");
globalThis.__imageCurrentRequestObservations = [];
image.src = "data:,c";
const beforeUpdate = image.currentSrc;
queueMicrotask(() => {
__imageCurrentRequestObservations.push(`first:${image.currentSrc}`);
picture.append(image);
__imageCurrentRequestObservations.push(`inserted:${image.currentSrc}`);
queueMicrotask(() => {
__imageCurrentRequestObservations.push(`second:${image.currentSrc}`);
});
});
return beforeUpdate;
})()
"#,
)
.expect("image current request setup should evaluate");
assert_eq!(before_update, "");
assert_eq!(
vm.eval("globalThis.__imageCurrentRequestObservations.join('|')")
.expect("image current request observations should evaluate"),
"first:data:,c|inserted:data:,c|second:data:,b"
);
}
#[tokio::test]
async fn image_current_src_is_empty_while_a_replacement_request_is_pending() {
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://image-current-request.test/page.html",
&loader,
);
vm.set_fetch_subresource_interception(true, Some(crate::types::SubresourceResourceType::Image));
vm.eval(
r#"
(() => {
globalThis.__pendingCurrentRequestImage = new Image();
__pendingCurrentRequestImage.src = "data:,old";
})()
"#,
)
.expect("initial synthetic image request should evaluate");
assert_eq!(
vm.eval("globalThis.__pendingCurrentRequestImage.currentSrc")
.expect("initial current source should evaluate"),
"data:,old"
);
assert_eq!(
vm.eval(
r#"
(() => {
const before = __pendingCurrentRequestImage.currentSrc;
__pendingCurrentRequestImage.src = "replacement.gif";
return `${before}|${__pendingCurrentRequestImage.currentSrc}`;
})()
"#,
)
.expect("replacement image request should evaluate"),
"data:,old|data:,old"
);
let pending = vm.take_pending_subresource_fetch_infos();
assert_eq!(pending.len(), 1);
assert_eq!(
vm.eval("globalThis.__pendingCurrentRequestImage.currentSrc")
.expect("pending current source should evaluate"),
""
);
vm.fulfill_pending_subresource_fetch(
pending[0].internal_id,
200,
vec![("Content-Type".to_owned(), "image/gif".to_owned())],
one_by_one_gif_response_body(),
)
.expect("replacement image request should fulfill");
assert_eq!(
vm.eval("globalThis.__pendingCurrentRequestImage.currentSrc")
.expect("completed current source should evaluate"),
"https://image-current-request.test/replacement.gif"
);
}
#[tokio::test]
async fn image_fetch_enabled_dispatches_http_error_after_resource_timing() {
let (base_url, request_rx, server) =
@@ -488,6 +599,81 @@ async fn inserting_completed_detached_image_does_not_restart_request() {
);
}
#[tokio::test]
async fn image_update_microtask_freezes_request_after_observing_live_base_url() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
loader.set_image_fetch_enabled(true);
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
"https://image-update-base.test/page.html",
&loader,
);
vm.set_fetch_subresource_interception(true, Some(crate::types::SubresourceResourceType::Image));
vm.eval(
r#"
(() => {
const image = new Image();
globalThis.__baseSelectedImage = image;
globalThis.__baseSelectedImageEvents = [];
image.onload = () => __baseSelectedImageEvents.push("load");
image.onerror = () => __baseSelectedImageEvents.push("error");
image.src = "green.png";
const root = document.documentElement ||
document.appendChild(document.createElement("html"));
const head = document.head ||
root.insertBefore(document.createElement("head"), root.firstChild);
const base = document.createElement("base");
base.href = "/resources/";
head.appendChild(base);
globalThis.__baseSelectedImageBase = base;
})()
"#,
)
.expect("image and base should update in one script turn");
let pending = vm.take_pending_subresource_fetch_infos();
assert_eq!(pending.len(), 1);
assert_eq!(
pending[0].url.as_str(),
"https://image-update-base.test/resources/green.png",
"URL selection must happen in the queued image-update microtask"
);
vm.fulfill_pending_subresource_fetch(
pending[0].internal_id,
200,
vec![("Content-Type".to_owned(), "image/gif".to_owned())],
one_by_one_gif_response_body(),
)
.expect("base-selected image response should complete");
vm.eval(
r#"
(() => {
__baseSelectedImageBase.href = "/bogus/";
(document.body || document.documentElement).appendChild(__baseSelectedImage);
})()
"#,
)
.expect("completed detached image should insert after a later base change");
assert!(
vm.take_pending_subresource_fetch_infos().is_empty(),
"ordinary insertion must preserve the request selected by the update microtask"
);
run_next_image_event_task(&mut vm, &loader, "base-selected image event").await;
assert_eq!(
vm.eval(
r#"__baseSelectedImageEvents.join("|") + ":" +
performance.getEntriesByName(
"https://image-update-base.test/resources/green.png"
).length"#,
)
.expect("base-selected image result should evaluate"),
"load:1"
);
}
#[tokio::test]
async fn removed_lazy_image_suppresses_in_flight_terminal_event() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
@@ -517,6 +703,8 @@ async fn removed_lazy_image_suppresses_in_flight_terminal_event() {
vm.refresh_layout_snapshot_for_test(moli_layout::LayoutViewport::new(800, 600, 1.0,))
.expect("near lazy-image layout refresh should succeed")
);
vm.perform_owner_lane_task_microtask_checkpoints()
.expect("layout task should finish its image-update microtask checkpoint");
let pending = vm.take_pending_subresource_fetch_infos();
assert_eq!(pending.len(), 1);
@@ -852,7 +1040,7 @@ async fn image_request_interception_fulfills_through_image_continuation() {
}
#[tokio::test]
async fn image_decode_waits_for_in_flight_pixels_and_reuses_the_ready_resource() {
async fn image_decode_quick_attach_distinguishes_uncached_and_ready_requests() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
loader.set_image_fetch_enabled(true);
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
@@ -883,8 +1071,8 @@ async fn image_decode_waits_for_in_flight_pixels_and_reuses_the_ready_resource()
assert_eq!(pending.len(), 1);
assert_eq!(
vm.eval("globalThis.__firstSharedDecode")
.expect("in-flight image decode state should evaluate"),
"pending"
.expect("uncached quick-attach image decode state should evaluate"),
"reject:EncodingError"
);
vm.fulfill_pending_subresource_fetch(
pending[0].internal_id,
@@ -897,7 +1085,7 @@ async fn image_decode_waits_for_in_flight_pixels_and_reuses_the_ready_resource()
assert_eq!(
vm.eval("globalThis.__firstSharedDecode")
.expect("first image decode result should evaluate"),
"resolve"
"reject:EncodingError"
);
let first_image = vm
.document_runtime
@@ -2417,6 +2417,7 @@ fn html_image_data_gif_exposes_intrinsic_dimensions() {
r#"
(() => {
const img = new Image(4, 5);
globalThis.__lmDataGifImage = img;
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
return [
img.complete,
@@ -2430,7 +2431,16 @@ fn html_image_data_gif_exposes_intrinsic_dimensions() {
)
.expect("data image dimensions should be readable");
assert_eq!(result, "true|4|5|1|1");
assert_eq!(result, "true|4|5|0|0");
assert_eq!(
vm.eval(
"[__lmDataGifImage.complete, __lmDataGifImage.width, \
__lmDataGifImage.height, __lmDataGifImage.naturalWidth, \
__lmDataGifImage.naturalHeight].join('|')",
)
.expect("data image dimensions should settle at the task checkpoint"),
"true|4|5|1|1"
);
}
#[test]
@@ -2493,6 +2503,13 @@ fn html_image_data_svg_exposes_intrinsic_dimensions() {
const fractionalConcreteHeight = new Image();
fractionalConcreteHeight.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 12"/>';
globalThis.__lmDataSvgImages = {
sized,
ratioWidth,
noSize,
fractionalConcreteHeight
};
return [
[sized.naturalWidth, sized.naturalHeight].join('x'),
[ratioWidth.naturalWidth, ratioWidth.naturalHeight].join('x'),
@@ -2504,6 +2521,24 @@ fn html_image_data_svg_exposes_intrinsic_dimensions() {
)
.expect("data SVG image dimensions should be readable");
assert_eq!(result, "0x0|0x0|0x0|0x0");
let result = vm
.eval(
r#"
(() => {
const { sized, ratioWidth, noSize, fractionalConcreteHeight } = __lmDataSvgImages;
return [
[sized.naturalWidth, sized.naturalHeight].join('x'),
[ratioWidth.naturalWidth, ratioWidth.naturalHeight].join('x'),
[noSize.naturalWidth, noSize.naturalHeight].join('x'),
[fractionalConcreteHeight.naturalWidth, fractionalConcreteHeight.naturalHeight].join('x')
].join('|');
})()
"#,
)
.expect("data SVG dimensions should settle at the task checkpoint");
// Blink resolves an external SVG with neither dimensions nor a viewBox
// against the replaced-element default object size.
// Its DOM natural dimensions remain integers even when layout retains the
@@ -2586,7 +2621,7 @@ async fn html_image_load_runs_on_its_dom_task_not_window_load_dispatch() {
)
.expect("image setup should evaluate");
assert_eq!(before_load, "1|1|1|1|pending");
assert_eq!(before_load, "0|0|0|0|pending");
vm.dispatch_window_load_event()
.expect("window load should not dispatch pending image loads");
@@ -2737,8 +2772,8 @@ async fn html_image_complete_tracks_pending_src_and_srcset_loads() {
assert_eq!(
drain_canvas_image_load_event_tasks(&mut vm, &loader).await,
4,
"src, srcset, empty-src, and retired-source work must each settle one queued task"
3,
"src, srcset, and empty-src work must settle queued tasks; the retired source is cancelled before its update microtask can manufacture one"
);
let after_load = vm
@@ -3034,6 +3069,8 @@ async fn parser_image_load_honors_meta_img_src_csp() {
.expect("parser EOF should prepare interactive");
vm.apply_main_document_interactive_lifecycle_action(interactive)
.expect("interactive transition should register the parser image");
vm.perform_script_task_checkpoint(None)
.expect("parser task-end checkpoint should select the image request");
run_one_canvas_image_load_event_task(&mut vm, &loader).await;
assert_eq!(
drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm),
@@ -3626,6 +3663,8 @@ async fn html_image_lazy_detached_sources_wait_until_inserted() {
vm.refresh_layout_snapshot_for_test(moli_layout::LayoutViewport::new(800, 600, 1.0,))
.expect("inserted lazy-image layout refresh should succeed")
);
vm.perform_owner_lane_task_microtask_checkpoints()
.expect("layout task should finish its image-update microtask checkpoint");
assert!(
drain_canvas_image_load_event_tasks(&mut vm, &loader).await > 0,
"inserted lazy image events should dispatch"
@@ -3719,6 +3758,8 @@ async fn html_image_below_viewport_lazy_waits_until_scroll_reveal() {
vm.refresh_layout_snapshot_for_test(moli_layout::LayoutViewport::new(800, 600, 1.0,))
.expect("initial lazy-image layout refresh should succeed")
);
vm.perform_owner_lane_task_microtask_checkpoints()
.expect("layout task should finish its image-update microtask checkpoint");
assert!(
drain_canvas_image_load_event_tasks(&mut vm, &loader).await > 0,
"the near-viewport lazy image should be admitted"
@@ -12133,7 +12133,7 @@ async fn child_image_network_failure_releases_lifecycle_before_later_host_load()
assert!(
vm._context_host
.borrow()
.pending_image_load_event_is_current(child_image, detached_pending),
.pending_image_load_event_is_current(child_image, &detached_pending),
"same-document detach must preserve current image ownership"
);
assert!(