diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 3df01ee978..c0c1173051 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -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 diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 7056c50268..a82370d948 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -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 diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index e0f1019800..76792e27c9 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -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 { - 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, + ) { + 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; diff --git a/moli-renderer-v8/src/native_bridge/context_host/image_decodes.rs b/moli-renderer-v8/src/native_bridge/context_host/image_decodes.rs index e1732c63ac..644ba4f42f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/image_decodes.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/image_decodes.rs @@ -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)) } diff --git a/moli-renderer-v8/src/native_bridge/context_host/image_loads.rs b/moli-renderer-v8/src/native_bridge/context_host/image_loads.rs index e9b5e1a289..5dd648d09f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/image_loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/image_loads.rs @@ -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 { - 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 { - 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 diff --git a/moli-renderer-v8/src/native_bridge/context_host/image_resources/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/image_resources/mod.rs index bbf542d65e..9efe9cf511 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/image_resources/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/image_resources/mod.rs @@ -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, 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, 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 } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 8541bb9e55..3b99e6c4f5 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -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, 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 { + pub(crate) fn request_key(&self) -> Option<&ImageRequestKey> { + self.request_key.as_ref() + } + + pub(crate) fn network_request_id(&self) -> Option { 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 { + pub(crate) fn terminal_source(&self) -> Option { 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 { 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, 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, pending_media_load_sequences: HashMap, next_media_load_sequence_id: u64, pending_text_track_load_sequences: HashMap, diff --git a/moli-renderer-v8/src/native_bridge/element/images/src.rs b/moli-renderer-v8/src/native_bridge/element/images/src.rs index ce9873cbaa..5a2320687d 100644 --- a/moli-renderer-v8/src/native_bridge/element/images/src.rs +++ b/moli-renderer-v8/src/native_bridge/element/images/src.rs @@ -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::::try_from(data).ok()?; + let handle = data.get_index(scope, IMAGE_UPDATE_HANDLE_INDEX)?; + let handle = v8::Local::::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::::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 { 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)] diff --git a/moli-renderer-v8/src/network_host/image.rs b/moli-renderer-v8/src/network_host/image.rs index e9a59c65de..0f26b7bcde 100644 --- a/moli-renderer-v8/src/network_host/image.rs +++ b/moli-renderer-v8/src/network_host/image.rs @@ -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()) => diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/image_load_event.rs b/moli-renderer-v8/src/runtime/page_vm/tests/image_load_event.rs index f1a6e1ea6f..486259e2de 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/image_load_event.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/image_load_event.rs @@ -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>(()) }) diff --git a/moli-renderer-v8/src/runtime/phase_one/mod.rs b/moli-renderer-v8/src/runtime/phase_one/mod.rs index a3a2005b11..bfc5f2a0bb 100644 --- a/moli-renderer-v8/src/runtime/phase_one/mod.rs +++ b/moli-renderer-v8/src/runtime/phase_one/mod.rs @@ -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" ); })); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/images.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/images.rs index 68a13102ba..bbd26afbb3 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/images.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/images.rs @@ -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 diff --git a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs index a515aed40e..d2fb96a25a 100644 --- a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs +++ b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs @@ -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,'; + 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" diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs index a0000dcdd8..0a82d081d4 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs @@ -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!(