Add tests to bevy_ui::Layout (#9781)
# Objective Add tests for `ui_layout_system` and `UiSurface` to the `bevy_ui::Layout` module. ## Solution Spawn a dummy window entity with `Window` and `PrimaryWindow` components so that `ui_layout_system` can run in a test without a window present. --- ## Changelog Added tests to the `bevy_ui::layout` module.
This commit is contained in:
parent
b900b97aa2
commit
97eda02f42
@ -396,10 +396,322 @@ fn round_layout_coords(value: Vec2) -> Vec2 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::layout::round_layout_coords;
|
||||
use crate::prelude::*;
|
||||
use crate::ui_layout_system;
|
||||
use crate::ContentSize;
|
||||
use crate::UiSurface;
|
||||
use bevy_ecs::event::Events;
|
||||
use bevy_ecs::schedule::Schedule;
|
||||
use bevy_ecs::world::World;
|
||||
use bevy_hierarchy::despawn_with_children_recursive;
|
||||
use bevy_hierarchy::BuildWorldChildren;
|
||||
use bevy_math::vec2;
|
||||
use bevy_math::Vec2;
|
||||
use bevy_utils::prelude::default;
|
||||
use bevy_utils::HashMap;
|
||||
use bevy_window::PrimaryWindow;
|
||||
use bevy_window::Window;
|
||||
use bevy_window::WindowResized;
|
||||
use bevy_window::WindowResolution;
|
||||
use bevy_window::WindowScaleFactorChanged;
|
||||
use taffy::tree::LayoutTree;
|
||||
|
||||
#[test]
|
||||
fn round_layout_coords_must_round_ties_up() {
|
||||
assert_eq!(round_layout_coords(vec2(-50.5, 49.5)), vec2(-50., 50.));
|
||||
}
|
||||
|
||||
// these window dimensions are easy to convert to and from percentage values
|
||||
const WINDOW_WIDTH: f32 = 1000.;
|
||||
const WINDOW_HEIGHT: f32 = 100.;
|
||||
|
||||
fn setup_ui_test_world() -> (World, Schedule) {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<UiScale>();
|
||||
world.init_resource::<UiSurface>();
|
||||
world.init_resource::<Events<WindowScaleFactorChanged>>();
|
||||
world.init_resource::<Events<WindowResized>>();
|
||||
|
||||
// spawn a dummy primary window
|
||||
world.spawn((
|
||||
Window {
|
||||
resolution: WindowResolution::new(WINDOW_WIDTH, WINDOW_HEIGHT),
|
||||
..Default::default()
|
||||
},
|
||||
PrimaryWindow,
|
||||
));
|
||||
|
||||
let mut ui_schedule = Schedule::default();
|
||||
ui_schedule.add_systems(ui_layout_system);
|
||||
|
||||
(world, ui_schedule)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_nodes_with_percent_100_dimensions_should_fill_their_parent() {
|
||||
let (mut world, mut ui_schedule) = setup_ui_test_world();
|
||||
|
||||
// spawn a root entity with width and height set to fill 100% of its parent
|
||||
let ui_root = world
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.id();
|
||||
|
||||
let ui_child = world
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.id();
|
||||
|
||||
world.entity_mut(ui_root).add_child(ui_child);
|
||||
|
||||
ui_schedule.run(&mut world);
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
|
||||
for ui_entity in [ui_root, ui_child] {
|
||||
let layout = ui_surface.get_layout(ui_entity).unwrap();
|
||||
assert_eq!(layout.size.width, WINDOW_WIDTH);
|
||||
assert_eq!(layout.size.height, WINDOW_HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_surface_tracks_ui_entities() {
|
||||
let (mut world, mut ui_schedule) = setup_ui_test_world();
|
||||
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
// no UI entities in world, none in UiSurface
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
assert!(ui_surface.entity_to_taffy.is_empty());
|
||||
|
||||
let ui_entity = world.spawn(NodeBundle::default()).id();
|
||||
|
||||
// `ui_layout_system` should map `ui_entity` to a ui node in `UiSurface::entity_to_taffy`
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity));
|
||||
assert_eq!(ui_surface.entity_to_taffy.len(), 1);
|
||||
|
||||
world.despawn(ui_entity);
|
||||
|
||||
// `ui_layout_system` should remove `ui_entity` from `UiSurface::entity_to_taffy`
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
assert!(!ui_surface.entity_to_taffy.contains_key(&ui_entity));
|
||||
assert!(ui_surface.entity_to_taffy.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn despawning_a_ui_entity_should_remove_its_corresponding_ui_node() {
|
||||
let (mut world, mut ui_schedule) = setup_ui_test_world();
|
||||
|
||||
let ui_entity = world.spawn(NodeBundle::default()).id();
|
||||
|
||||
// `ui_layout_system` will insert a ui node into the internal layout tree corresponding to `ui_entity`
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
// retrieve the ui node corresponding to `ui_entity` from ui surface
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
let ui_node = ui_surface.entity_to_taffy[&ui_entity];
|
||||
|
||||
world.despawn(ui_entity);
|
||||
|
||||
// `ui_layout_system` will recieve a `RemovedComponents<Node>` event for `ui_entity`
|
||||
// and remove `ui_entity` from `ui_node` from the internal layout tree
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
|
||||
// `ui_node` is removed, attempting to retrieve a style for `ui_node` panics
|
||||
let _ = ui_surface.taffy.style(ui_node);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changes_to_children_of_a_ui_entity_change_its_corresponding_ui_nodes_children() {
|
||||
let (mut world, mut ui_schedule) = setup_ui_test_world();
|
||||
|
||||
let ui_parent_entity = world.spawn(NodeBundle::default()).id();
|
||||
|
||||
// `ui_layout_system` will insert a ui node into the internal layout tree corresponding to `ui_entity`
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
let ui_parent_node = ui_surface.entity_to_taffy[&ui_parent_entity];
|
||||
|
||||
// `ui_parent_node` shouldn't have any children yet
|
||||
assert_eq!(ui_surface.taffy.child_count(ui_parent_node).unwrap(), 0);
|
||||
|
||||
let mut ui_child_entities = (0..10)
|
||||
.map(|_| {
|
||||
let child = world.spawn(NodeBundle::default()).id();
|
||||
world.entity_mut(ui_parent_entity).add_child(child);
|
||||
child
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
// `ui_parent_node` should have children now
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
assert_eq!(
|
||||
ui_surface.entity_to_taffy.len(),
|
||||
1 + ui_child_entities.len()
|
||||
);
|
||||
assert_eq!(
|
||||
ui_surface.taffy.child_count(ui_parent_node).unwrap(),
|
||||
ui_child_entities.len()
|
||||
);
|
||||
|
||||
let child_node_map = HashMap::from_iter(
|
||||
ui_child_entities
|
||||
.iter()
|
||||
.map(|child_entity| (*child_entity, ui_surface.entity_to_taffy[child_entity])),
|
||||
);
|
||||
|
||||
// the children should have a corresponding ui node and that ui node's parent should be `ui_parent_node`
|
||||
for node in child_node_map.values() {
|
||||
assert_eq!(ui_surface.taffy.parent(*node), Some(ui_parent_node));
|
||||
}
|
||||
|
||||
// delete every second child
|
||||
let mut deleted_children = vec![];
|
||||
for i in (0..ui_child_entities.len()).rev().step_by(2) {
|
||||
let child = ui_child_entities.remove(i);
|
||||
world.despawn(child);
|
||||
deleted_children.push(child);
|
||||
}
|
||||
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
assert_eq!(
|
||||
ui_surface.entity_to_taffy.len(),
|
||||
1 + ui_child_entities.len()
|
||||
);
|
||||
assert_eq!(
|
||||
ui_surface.taffy.child_count(ui_parent_node).unwrap(),
|
||||
ui_child_entities.len()
|
||||
);
|
||||
|
||||
// the remaining children should still have nodes in the layout tree
|
||||
for child_entity in &ui_child_entities {
|
||||
let child_node = child_node_map[child_entity];
|
||||
assert_eq!(ui_surface.entity_to_taffy[child_entity], child_node);
|
||||
assert_eq!(ui_surface.taffy.parent(child_node), Some(ui_parent_node));
|
||||
assert!(ui_surface
|
||||
.taffy
|
||||
.children(ui_parent_node)
|
||||
.unwrap()
|
||||
.contains(&child_node));
|
||||
}
|
||||
|
||||
// the nodes of the deleted children should have been removed from the layout tree
|
||||
for deleted_child_entity in &deleted_children {
|
||||
assert!(!ui_surface
|
||||
.entity_to_taffy
|
||||
.contains_key(deleted_child_entity));
|
||||
let deleted_child_node = child_node_map[deleted_child_entity];
|
||||
assert!(!ui_surface
|
||||
.taffy
|
||||
.children(ui_parent_node)
|
||||
.unwrap()
|
||||
.contains(&deleted_child_node));
|
||||
}
|
||||
|
||||
// despawn the parent entity and its descendants
|
||||
despawn_with_children_recursive(&mut world, ui_parent_entity);
|
||||
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
// all nodes should have been deleted
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
assert!(ui_surface.entity_to_taffy.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_node_should_be_set_to_its_content_size() {
|
||||
let (mut world, mut ui_schedule) = setup_ui_test_world();
|
||||
|
||||
let content_size = Vec2::new(50., 25.);
|
||||
|
||||
let ui_entity = world
|
||||
.spawn((
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
align_self: AlignSelf::Start,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
ContentSize::fixed_size(content_size),
|
||||
))
|
||||
.id();
|
||||
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
let layout = ui_surface.get_layout(ui_entity).unwrap();
|
||||
|
||||
// the node should takes its size from the fixed size measure func
|
||||
assert_eq!(layout.size.width, content_size.x);
|
||||
assert_eq!(layout.size.height, content_size.y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measure_funcs_should_be_removed_on_content_size_removal() {
|
||||
let (mut world, mut ui_schedule) = setup_ui_test_world();
|
||||
|
||||
let content_size = Vec2::new(50., 25.);
|
||||
let ui_entity = world
|
||||
.spawn((
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
align_self: AlignSelf::Start,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
ContentSize::fixed_size(content_size),
|
||||
))
|
||||
.id();
|
||||
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
let ui_node = ui_surface.entity_to_taffy[&ui_entity];
|
||||
|
||||
// a node with a content size needs to be measured
|
||||
assert!(ui_surface.taffy.needs_measure(ui_node));
|
||||
let layout = ui_surface.get_layout(ui_entity).unwrap();
|
||||
assert_eq!(layout.size.width, content_size.x);
|
||||
assert_eq!(layout.size.height, content_size.y);
|
||||
|
||||
world.entity_mut(ui_entity).remove::<ContentSize>();
|
||||
|
||||
ui_schedule.run(&mut world);
|
||||
|
||||
let ui_surface = world.resource::<UiSurface>();
|
||||
// a node without a content size does not need to be measured
|
||||
assert!(!ui_surface.taffy.needs_measure(ui_node));
|
||||
|
||||
// Without a content size, the node has no width or height constraints so the length of both dimensions is 0.
|
||||
let layout = ui_surface.get_layout(ui_entity).unwrap();
|
||||
assert_eq!(layout.size.width, 0.);
|
||||
assert_eq!(layout.size.height, 0.);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user