use bevy::prelude::*;
use bevy::ui::widget::{ImageNode, NodeImageMode};
use super::test_util::{ent, op_app, update_delta};
use crate::protocol::{op::Op, props::Props};
use crate::svg::SvgSurface;
fn image_create(id: u32, props: serde_json::Value) -> Op {
Op::Create {
id,
kind: "image".into(),
props: Props::decode_for("image", props),
text: None,
}
}
fn src_props(src: &str) -> Props {
*Props::decode_for("image", serde_json::json!({ "src": src }))
}
#[test]
fn svg_src_mounts_element_owned_surface() {
let (mut app, tx) = op_app();
tx.send(vec![image_create(
1,
serde_json::json!({ "src": "icons/a.svg", "tint": "red" }),
)])
.unwrap();
app.update();
let e = ent(&app, 1);
let entity = app.world().entity(e);
let surface = entity
.get::<SvgSurface>()
.expect("svg mode stamps an SvgSurface");
assert!(surface.dirty, "a fresh surface awaits its first raster");
assert_eq!(surface.last_size, UVec2::ZERO);
let img = entity
.get::<ImageNode>()
.expect("svg mode is backed by an ImageNode");
assert!(
matches!(img.image_mode, NodeImageMode::Stretch),
"the raster stretches to the laid-out box"
);
let path_image: Handle<Image> = app.world().resource::<AssetServer>().load("icons/a.svg");
assert_ne!(
img.image, path_image,
"the texture is element-owned, never the path loaded as an Image"
);
assert_eq!(
img.color,
crate::ui_map::parse_color("red"),
"tint applies in svg mode"
);
}
#[test]
fn svg_detection_is_case_insensitive() {
let (mut app, tx) = op_app();
tx.send(vec![image_create(1, serde_json::json!({ "src": "B.SVG" }))])
.unwrap();
app.update();
assert!(
app.world()
.entity(ent(&app, 1))
.get::<SvgSurface>()
.is_some(),
"an uppercase .SVG src must still enter svg mode"
);
}
#[test]
fn raster_src_stays_plain_image() {
let (mut app, tx) = op_app();
tx.send(vec![image_create(
1,
serde_json::json!({ "src": "icons/a.png" }),
)])
.unwrap();
app.update();
let e = ent(&app, 1);
assert!(
app.world().entity(e).get::<SvgSurface>().is_none(),
"a raster image must not carry an SvgSurface"
);
let expected: Handle<Image> = app.world().resource::<AssetServer>().load("icons/a.png");
assert_eq!(
app.world().entity(e).get::<ImageNode>().unwrap().image,
expected,
"a raster src loads the path as the texture"
);
}
#[test]
fn src_swaps_transition_between_modes() {
let (mut app, tx) = op_app();
tx.send(vec![image_create(
1,
serde_json::json!({ "src": "icons/a.png" }),
)])
.unwrap();
app.update();
let e = ent(&app, 1);
tx.send(vec![update_delta(1, src_props("icons/a.svg"), &[], &[])])
.unwrap();
app.update();
{
let entity = app.world().entity(e);
assert!(
entity.get::<SvgSurface>().is_some(),
"swapping to an .svg src must insert an SvgSurface"
);
let img = entity.get::<ImageNode>().unwrap();
assert!(matches!(img.image_mode, NodeImageMode::Stretch));
let png: Handle<Image> = app.world().resource::<AssetServer>().load("icons/a.png");
assert_ne!(
img.image, png,
"the loaded png texture must be replaced by an element-owned blank"
);
}
tx.send(vec![update_delta(1, src_props("icons/a.png"), &[], &[])])
.unwrap();
app.update();
let entity = app.world().entity(e);
assert!(
entity.get::<SvgSurface>().is_none(),
"swapping back to a raster src must remove the SvgSurface"
);
let expected: Handle<Image> = app.world().resource::<AssetServer>().load("icons/a.png");
assert_eq!(
entity.get::<ImageNode>().unwrap().image,
expected,
"the plain texture-loading rebuild path is restored"
);
}
#[test]
fn same_src_update_keeps_doc_handle_and_dirty_state() {
let (mut app, tx) = op_app();
tx.send(vec![image_create(
1,
serde_json::json!({ "src": "icons/a.svg" }),
)])
.unwrap();
app.update();
let e = ent(&app, 1);
let doc_id = app.world().entity(e).get::<SvgSurface>().unwrap().doc.id();
app.world_mut()
.entity_mut(e)
.get_mut::<SvgSurface>()
.unwrap()
.dirty = false;
let texture_before = app.world().entity(e).get::<ImageNode>().unwrap().image.id();
let surface_tick = app
.world()
.entity(e)
.get_change_ticks::<SvgSurface>()
.unwrap()
.changed;
tx.send(vec![update_delta(
1,
*Props::decode_for("image", serde_json::json!({ "tint": "blue" })),
&[],
&[],
)])
.unwrap();
app.update();
{
let entity = app.world().entity(e);
let surface = entity.get::<SvgSurface>().unwrap();
assert_eq!(
surface.doc.id(),
doc_id,
"an unchanged src must keep the same document handle"
);
assert!(
!surface.dirty,
"an unchanged src must not re-flag the surface dirty"
);
assert_eq!(
entity.get::<ImageNode>().unwrap().image.id(),
texture_before,
"the element-owned texture must be reused, not reallocated"
);
assert_eq!(
entity.get_change_ticks::<SvgSurface>().unwrap().changed,
surface_tick,
"a same-doc rebuild must not even flag Changed<SvgSurface>"
);
}
tx.send(vec![update_delta(1, src_props("icons/b.svg"), &[], &[])])
.unwrap();
app.update();
let surface = app.world().entity(e).get::<SvgSurface>().unwrap();
assert_ne!(
surface.doc.id(),
doc_id,
"a different .svg src must swap the document handle"
);
assert!(surface.dirty, "a swapped document must re-flag dirty");
}
#[cfg(all(feature = "devtools", debug_assertions))]
#[test]
fn svg_mode_warns_on_atlas_and_source_rect() {
let _lock = crate::diag::test_lock();
crate::diag::arm_runtime();
let _ = crate::diag::take_runtime_warnings();
let (mut app, tx) = op_app();
tx.send(vec![image_create(
1,
serde_json::json!({
"src": "icons/a.svg",
"atlas": { "tileWidth": 8, "tileHeight": 8, "columns": 2, "rows": 2 },
}),
)])
.unwrap();
app.update();
let warns = crate::diag::take_runtime_warnings();
assert!(
warns
.iter()
.any(|w| w.kind == "svgImageAttrs" && w.node == Some(1) && w.value == "atlas"),
"create with atlas in svg mode must warn: {warns:?}"
);
tx.send(vec![update_delta(
1,
*Props::decode_for(
"image",
serde_json::json!({
"sourceRect": { "x": 0.0, "y": 0.0, "width": 4.0, "height": 4.0 },
}),
),
&[],
&[],
)])
.unwrap();
app.update();
let warns = crate::diag::take_runtime_warnings();
assert!(
warns
.iter()
.any(|w| w.kind == "svgImageAttrs" && w.node == Some(1) && w.value == "sourceRect"),
"update adding sourceRect in svg mode must warn: {warns:?}"
);
}