mirror of
https://github.com/lexmount/moli.git
synced 2026-10-26 00:01:50 +00:00
fix(images): track current request transitions
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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>(())
|
||||
})
|
||||
|
||||
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user