use bevy::picking::Pickable;
use bevy::prelude::*;
use bevy::text::TextCursorStyle;
use bevy::ui::FocusPolicy;
use bevy::ui::widget::ImageNode;
use crate::layer::LayerContentDirt;
use crate::protocol::op::Op;
use crate::protocol::props::Props;
use crate::reconcile::test_util::{ent, op_app, update_delta};
use crate::ui_map::parse_color;
fn props(json: serde_json::Value) -> Props {
serde_json::from_value(json).expect("valid props")
}
fn create(id: u32, kind: &str, json: serde_json::Value) -> Op {
Op::Create {
id,
kind: kind.into(),
props: Props::decode_for(kind, json),
text: None,
}
}
#[test]
fn button_keeps_block_focus_across_hover() {
let (mut app, ops_tx) = op_app();
app.add_systems(
Update,
crate::reconcile::apply_interaction_styles.after(crate::reconcile::apply_js_ops),
);
ops_tx
.send(vec![create(
1,
"button",
serde_json::json!({ "hoverStyle": { "backgroundColor": "red" } }),
)])
.unwrap();
app.update();
let button = ent(&app, 1);
app.world_mut()
.entity_mut(button)
.insert(Interaction::Hovered);
app.update();
let entity = app.world().entity(button);
assert_eq!(entity.get::<FocusPolicy>(), Some(&FocusPolicy::Block));
assert!(
entity
.get::<Pickable>()
.is_some_and(|p| p.should_block_lower),
"the picking mirror follows the button default"
);
}
#[test]
fn root_stays_unpickable_after_focus_policy_update() {
let (mut app, ops_tx) = op_app();
ops_tx
.send(vec![create(1, "root", serde_json::json!({}))])
.unwrap();
app.update();
ops_tx
.send(vec![update_delta(
1,
props(serde_json::json!({ "style": { "focusPolicy": "pass" } })),
&[],
&[],
)])
.unwrap();
app.update();
let root = ent(&app, 1);
assert_eq!(
app.world().entity(root).get::<Pickable>(),
Some(&Pickable::IGNORE)
);
}
#[test]
fn editable_text_applies_color_and_font_updates() {
let (mut app, ops_tx) = op_app();
ops_tx
.send(vec![create(
1,
"editableText",
serde_json::json!({ "style": { "color": "red", "fontSize": 12 } }),
)])
.unwrap();
app.update();
ops_tx
.send(vec![update_delta(
1,
props(serde_json::json!({ "style": { "color": "blue", "fontSize": 30 } })),
&[],
&[],
)])
.unwrap();
app.update();
let entity = app.world().entity(ent(&app, 1));
assert_eq!(
entity.get::<TextColor>().map(|c| c.0),
Some(parse_color("blue"))
);
assert_eq!(
entity.get::<TextCursorStyle>().map(|c| c.color),
Some(parse_color("blue")),
"the caret follows the text color"
);
let font = entity.get::<TextFont>().expect("font");
assert_eq!(font.font_size, bevy::text::FontSize::Px(30.0));
}
#[test]
fn image_refolds_opacity_on_opacity_delta() {
let (mut app, ops_tx) = op_app();
ops_tx
.send(vec![create(
1,
"image",
serde_json::json!({ "style": { "opacity": 1.0 } }),
)])
.unwrap();
app.update();
ops_tx
.send(vec![update_delta(
1,
props(serde_json::json!({ "style": { "opacity": 0.5 } })),
&[],
&[],
)])
.unwrap();
app.update();
let image = app.world().entity(ent(&app, 1)).get::<ImageNode>().unwrap();
assert!(
(image.color.alpha() - 0.5).abs() < 1e-5,
"opacity re-folded into the tint, got {}",
image.color.alpha()
);
}
#[test]
fn image_refolds_opacity_on_hover() {
let (mut app, ops_tx) = op_app();
app.add_systems(
Update,
crate::reconcile::apply_interaction_styles.after(crate::reconcile::apply_js_ops),
);
ops_tx
.send(vec![create(
1,
"image",
serde_json::json!({ "tint": "red", "hoverStyle": { "opacity": 0.25 } }),
)])
.unwrap();
app.update();
let image = ent(&app, 1);
app.world_mut()
.entity_mut(image)
.insert(Interaction::Hovered);
app.update();
let color = app.world().entity(image).get::<ImageNode>().unwrap().color;
assert!(
(color.alpha() - 0.25).abs() < 1e-5,
"alpha {}",
color.alpha()
);
assert_eq!(
color.with_alpha(1.0),
parse_color("red"),
"the tint survives the re-fold"
);
}
#[test]
fn span_recolor_pushes_layer_content_dirt() {
let (mut app, ops_tx) = op_app();
app.init_resource::<LayerContentDirt>();
ops_tx
.send(vec![
create(1, "text", serde_json::json!({})),
create(
2,
"textSpan",
serde_json::json!({ "style": { "color": "red" } }),
),
Op::Append {
parent: 1,
child: 2,
},
])
.unwrap();
app.update();
app.world_mut()
.resource_mut::<LayerContentDirt>()
.nodes
.clear();
ops_tx
.send(vec![update_delta(
2,
props(serde_json::json!({ "style": { "color": "blue" } })),
&[],
&[],
)])
.unwrap();
app.update();
let span = ent(&app, 2);
assert!(
app.world()
.resource::<LayerContentDirt>()
.nodes
.contains(&span),
"the recolored span is content dirt"
);
assert_eq!(
app.world().entity(span).get::<TextColor>().map(|c| c.0),
Some(parse_color("blue"))
);
}