Track responsive image candidates in lifecycle
This commit is contained in:
1 parent
6f71c302c5
commit
bae1259afe
4 files changed
+365
-45
No files matched your search
@@ -2115,52 +2115,87 @@ fn op_image_metadata(state: &OpState, nid: u32, cached_only: bool) -> String {
|
||||
let shared = state.borrow::<SharedState>().clone();
|
||||
let mut gs = shared.borrow_mut();
|
||||
let node_id = NodeId::new(nid);
|
||||
let src = gs.dom.as_ref().and_then(|dom| {
|
||||
let node = dom.get_node(node_id)?;
|
||||
let element = node.as_element()?;
|
||||
if element.local.as_ref() != "img" {
|
||||
return None;
|
||||
}
|
||||
node.get_attribute("src").map(str::to_string)
|
||||
let is_image = gs.dom.as_ref().is_some_and(|dom| {
|
||||
dom.get_node(node_id)
|
||||
.is_some_and(|node| {
|
||||
node.as_element()
|
||||
.is_some_and(|element| element.local.as_ref() == "img")
|
||||
})
|
||||
});
|
||||
let Some(src) = src.filter(|src| !src.trim().is_empty()) else {
|
||||
if !is_image {
|
||||
return serde_json::json!({ "ok": false, "currentSrc": "" }).to_string();
|
||||
}
|
||||
let base_url = document_base_url(&gs);
|
||||
let viewport = gs.viewport;
|
||||
let ObscuraState {
|
||||
dom,
|
||||
render_resources,
|
||||
..
|
||||
} = &mut *gs;
|
||||
let Some(dom) = dom.as_ref() else {
|
||||
return serde_json::json!({ "ok": false, "currentSrc": "" }).to_string();
|
||||
};
|
||||
let base_url = document_base_url(&gs);
|
||||
let metadata = if cached_only {
|
||||
match gs
|
||||
.render_resources
|
||||
.cached_image_metadata(&src, base_url.as_deref())
|
||||
{
|
||||
None => {
|
||||
return serde_json::json!({ "state": "pending" }).to_string();
|
||||
}
|
||||
let (current_src, density, known, dimensions) = if cached_only {
|
||||
match render_resources.cached_image_element_metadata(
|
||||
dom,
|
||||
node_id,
|
||||
viewport,
|
||||
base_url.as_deref(),
|
||||
) {
|
||||
Some(metadata) => metadata,
|
||||
None => {
|
||||
return serde_json::json!({
|
||||
"state": "error",
|
||||
"ok": false,
|
||||
"currentSrc": "",
|
||||
})
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gs.render_resources
|
||||
.image_metadata(&src, base_url.as_deref())
|
||||
match render_resources.image_element_metadata(
|
||||
dom,
|
||||
node_id,
|
||||
viewport,
|
||||
base_url.as_deref(),
|
||||
) {
|
||||
Some((current_src, density, dimensions)) => {
|
||||
(current_src, density, true, dimensions)
|
||||
}
|
||||
None => {
|
||||
return serde_json::json!({
|
||||
"state": "error",
|
||||
"ok": false,
|
||||
"currentSrc": "",
|
||||
})
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
};
|
||||
match metadata {
|
||||
Some((current_src, width, height)) => serde_json::json!({
|
||||
if !known {
|
||||
return serde_json::json!({
|
||||
"state": "pending",
|
||||
"currentSrc": current_src,
|
||||
"density": density,
|
||||
})
|
||||
.to_string();
|
||||
}
|
||||
match dimensions {
|
||||
Some((width, height)) => serde_json::json!({
|
||||
"state": "loaded",
|
||||
"ok": true,
|
||||
"currentSrc": current_src,
|
||||
"density": density,
|
||||
"width": width,
|
||||
"height": height,
|
||||
})
|
||||
.to_string(),
|
||||
None => {
|
||||
let current_src = base_url
|
||||
.as_deref()
|
||||
.and_then(|base| url::Url::parse(base).ok())
|
||||
.and_then(|base| base.join(&src).ok())
|
||||
.map(|url| url.to_string())
|
||||
.unwrap_or(src);
|
||||
serde_json::json!({
|
||||
"state": "error",
|
||||
"ok": false,
|
||||
"currentSrc": current_src,
|
||||
"density": density,
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -4455,6 +4455,176 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn responsive_picture_lifecycle_tracks_viewport_density_and_source_media() {
|
||||
let requests = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let seen = requests.clone();
|
||||
let png = two_by_three_png();
|
||||
let mut rt = parser_image_runtime(
|
||||
r#"
|
||||
<picture>
|
||||
<source type="image/avif" srcset="unsupported.avif">
|
||||
<source id="wide-source" media="(min-width: 800px)"
|
||||
srcset="wide.png 2x">
|
||||
<source media="(max-width: 799px)" srcset="narrow.png">
|
||||
<img id="responsive-picture" src="fallback.png">
|
||||
</picture>
|
||||
"#,
|
||||
move |url: &str| {
|
||||
seen.lock().unwrap().push(url.to_string());
|
||||
Some(png.clone())
|
||||
},
|
||||
);
|
||||
rt.set_viewport(1000.0, 600.0);
|
||||
rt.execute_script(
|
||||
"observe-responsive-picture",
|
||||
r#"
|
||||
globalThis.__pictureLoads = [];
|
||||
const pictureImage = document.getElementById("responsive-picture");
|
||||
pictureImage.addEventListener("load", () => {
|
||||
__pictureLoads.push(pictureImage.currentSrc);
|
||||
});
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
rt.run_event_loop_bounded(100).await.unwrap();
|
||||
assert_eq!(
|
||||
rt.evaluate(
|
||||
"[pictureImage.currentSrc, pictureImage.naturalWidth, \
|
||||
pictureImage.naturalHeight, __pictureLoads]"
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::json!([
|
||||
"http://example.com/page/wide.png",
|
||||
1,
|
||||
2,
|
||||
["http://example.com/page/wide.png"]
|
||||
])
|
||||
);
|
||||
|
||||
// A live viewport change re-runs the renderer's media/source
|
||||
// selection. The cache-only complete getter must report pending but
|
||||
// must not perform the load itself.
|
||||
rt.set_viewport(600.0, 600.0);
|
||||
assert_eq!(rt.evaluate("pictureImage.complete").unwrap(), serde_json::json!(false));
|
||||
assert_eq!(requests.lock().unwrap().len(), 1);
|
||||
rt.run_event_loop_bounded(100).await.unwrap();
|
||||
assert_eq!(
|
||||
rt.evaluate(
|
||||
"[pictureImage.currentSrc, pictureImage.naturalWidth, \
|
||||
pictureImage.naturalHeight, __pictureLoads]"
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::json!([
|
||||
"http://example.com/page/narrow.png",
|
||||
2,
|
||||
3,
|
||||
[
|
||||
"http://example.com/page/wide.png",
|
||||
"http://example.com/page/narrow.png"
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
// Mutating a <source> selection input invalidates its associated img.
|
||||
// The wide bytes are already shared in the render cache, but lifecycle
|
||||
// completion remains task-queued and emits one new load event.
|
||||
rt.execute_script(
|
||||
"mutate-picture-source",
|
||||
r#"
|
||||
document.getElementById("wide-source").setAttribute("media", "all");
|
||||
globalThis.__pictureCompleteAfterSourceMutation = pictureImage.complete;
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
rt.evaluate("__pictureCompleteAfterSourceMutation").unwrap(),
|
||||
serde_json::json!(false)
|
||||
);
|
||||
rt.run_event_loop_bounded(100).await.unwrap();
|
||||
assert_eq!(
|
||||
rt.evaluate("[pictureImage.currentSrc, __pictureLoads]").unwrap(),
|
||||
serde_json::json!([
|
||||
"http://example.com/page/wide.png",
|
||||
[
|
||||
"http://example.com/page/wide.png",
|
||||
"http://example.com/page/narrow.png",
|
||||
"http://example.com/page/wide.png"
|
||||
]
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
*requests.lock().unwrap(),
|
||||
vec![
|
||||
"http://example.com/page/wide.png".to_string(),
|
||||
"http://example.com/page/narrow.png".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn responsive_srcset_sizes_uses_renderer_selected_current_src() {
|
||||
let requests = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let seen = requests.clone();
|
||||
let png = two_by_three_png();
|
||||
let mut rt = parser_image_runtime(
|
||||
r#"<img id="responsive-srcset" src="fallback.png"
|
||||
srcset="small.png 400w, large.png 800w" sizes="400px">"#,
|
||||
move |url: &str| {
|
||||
seen.lock().unwrap().push(url.to_string());
|
||||
Some(png.clone())
|
||||
},
|
||||
);
|
||||
rt.execute_script(
|
||||
"observe-responsive-srcset",
|
||||
r#"
|
||||
globalThis.__srcsetLoads = [];
|
||||
const srcsetImage = document.getElementById("responsive-srcset");
|
||||
srcsetImage.addEventListener("load", () => {
|
||||
__srcsetLoads.push(srcsetImage.currentSrc);
|
||||
});
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
rt.run_event_loop_bounded(100).await.unwrap();
|
||||
assert_eq!(
|
||||
rt.evaluate("[srcsetImage.currentSrc, __srcsetLoads]").unwrap(),
|
||||
serde_json::json!([
|
||||
"http://example.com/page/small.png",
|
||||
["http://example.com/page/small.png"]
|
||||
])
|
||||
);
|
||||
|
||||
change_srcset_image_sizes(&mut rt);
|
||||
assert_eq!(rt.evaluate("srcsetImage.complete").unwrap(), serde_json::json!(false));
|
||||
rt.run_event_loop_bounded(100).await.unwrap();
|
||||
assert_eq!(
|
||||
rt.evaluate("[srcsetImage.currentSrc, __srcsetLoads]").unwrap(),
|
||||
serde_json::json!([
|
||||
"http://example.com/page/large.png",
|
||||
[
|
||||
"http://example.com/page/small.png",
|
||||
"http://example.com/page/large.png"
|
||||
]
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
*requests.lock().unwrap(),
|
||||
vec![
|
||||
"http://example.com/page/small.png".to_string(),
|
||||
"http://example.com/page/large.png".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
fn change_srcset_image_sizes(rt: &mut ObscuraJsRuntime) {
|
||||
rt.execute_script("change-responsive-sizes", r#"srcsetImage.sizes = "800px";"#)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Regression for #105: `HTMLFormElement` must expose `.elements` so
|
||||
/// frameworks that probe form field collections work.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user