tests.rs
raw
use guiduck_scene::geom::Size;
use taffy::prelude::{length, percent};
use super::*;
use crate::text::TextContext;
use crate::widget::Container;
fn tree_with_root() -> (WidgetTree, WidgetId) {
let mut tree = WidgetTree::with_text_context(TextContext::hermetic([]));
let root = tree.insert(
Container::new(),
taffy::Style {
size: taffy::Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
flex_direction: taffy::FlexDirection::Column,
..Default::default()
},
None,
);
(tree, root)
}
fn row_style() -> taffy::Style {
taffy::Style {
size: taffy::Size {
width: percent(1.0_f32),
height: length(20.0_f32),
},
flex_shrink: 0.0,
..Default::default()
}
}
fn build_row(
tree: &mut WidgetTree,
parent: Option<WidgetId>,
at: usize,
_item: Signal<String>,
_index: Signal<i64>,
) -> Extent {
Extent::Single(tree.insert_at(Container::new(), row_style(), parent.expect("parent"), at))
}
/// The widget children of `parent` that are rows — everything between the
/// region's leading and trailing markers.
fn row_widgets(tree: &WidgetTree, parent: WidgetId) -> Vec<WidgetId> {
let children = tree.children(parent);
children[1..children.len() - 1].to_vec()
}
#[test]
fn keyed_reconcile_reuses_moves_and_disposes() {
let (mut tree, root) = tree_with_root();
let mut list: KeyedList<String, String> = KeyedList::new(&mut tree, Some(root));
let items: Vec<String> = ["a", "b", "c"].map(String::from).into();
list.reconcile(&mut tree, &items, |_, item| item.clone(), build_row);
tree.render_frame(Size::new(200.0, 200.0));
assert_eq!(list.len(), 3);
let [a, b, c] = row_widgets(&tree, root)[..] else {
panic!("three rows");
};
// Reorder plus removal plus insertion: c moves first, a survives, b
// vanishes, d appears at the end.
let items: Vec<String> = ["c", "a", "d"].map(String::from).into();
list.reconcile(&mut tree, &items, |_, item| item.clone(), build_row);
tree.render_frame(Size::new(200.0, 200.0));
let rows = row_widgets(&tree, root);
assert_eq!(rows.len(), 3);
assert_eq!(rows[0], c, "`c` kept its widget across the move");
assert_eq!(rows[1], a, "`a` kept its widget");
assert!(!tree.children(root).contains(&b), "`b` was removed");
assert_ne!(rows[2], b, "`d` is a fresh widget");
// Clearing removes everything but the marker.
list.reconcile(&mut tree, &[], |_, item| item.clone(), build_row);
tree.render_frame(Size::new(200.0, 200.0));
assert!(list.is_empty());
assert_eq!(
tree.children(root).len(),
2,
"only the region's own two markers remain"
);
}
#[test]
fn reused_rows_receive_item_and_index_updates() {
let (mut tree, root) = tree_with_root();
let mut list: KeyedList<i64, String> = KeyedList::new(&mut tree, Some(root));
let observed: std::rc::Rc<std::cell::RefCell<Vec<(i64, String)>>> = Default::default();
let items: Vec<String> = ["x:1", "y:2"].map(String::from).into();
let key_of = |_: usize, item: &String| item.split(':').nth(1).unwrap().parse::<i64>().unwrap();
let build = |tree: &mut WidgetTree,
parent: Option<WidgetId>,
at: usize,
item: Signal<String>,
index: Signal<i64>| {
let observed = observed.clone();
guiduck_signals::Effect::new(move || {
observed.borrow_mut().push((index.get(), item.get()));
});
Extent::Single(tree.insert_at(Container::new(), row_style(), parent.expect("parent"), at))
};
list.reconcile(&mut tree, &items, key_of, build);
guiduck_signals::flush_effects();
assert_eq!(
observed.borrow().as_slice(),
[(0, "x:1".into()), (1, "y:2".into())]
);
// Same keys, new values and order: rows are reused, their signals
// update — no new effects appear.
observed.borrow_mut().clear();
let items: Vec<String> = ["Y:2", "X:1"].map(String::from).into();
list.reconcile(&mut tree, &items, key_of, build);
guiduck_signals::flush_effects();
let mut seen = observed.borrow().clone();
seen.sort();
assert_eq!(
seen.as_slice(),
[(0, "Y:2".into()), (1, "X:1".into())],
"existing rows saw the new values and positions"
);
}
#[test]
fn conditional_swaps_branches_and_preserves_position() {
let (mut tree, root) = tree_with_root();
let before = tree.insert(Container::new(), row_style(), Some(root));
let mut conditional = Conditional::new(&mut tree, Some(root));
let after = tree.insert(Container::new(), row_style(), Some(root));
let build_then = |tree: &mut WidgetTree, parent: Option<WidgetId>, at: usize| {
Some(Extent::Single(tree.insert_at(
Container::new(),
row_style(),
parent.expect("parent"),
at,
)))
};
conditional.set(&mut tree, true, build_then);
tree.render_frame(Size::new(200.0, 200.0));
let children = tree.children(root).to_vec();
assert_eq!(children.len(), 5, "before, lead, branch, trail, after");
assert_eq!(children[0], before);
assert_eq!(children[4], after);
let branch = children[2];
// Flipping to a contentless else removes the branch.
conditional.set(&mut tree, false, |_, _, _| None);
tree.render_frame(Size::new(200.0, 200.0));
assert!(!tree.children(root).contains(&branch));
assert_eq!(tree.children(root).len(), 4);
// Same condition again: nothing rebuilds (the applied value is
// remembered even for empty branches).
conditional.set(&mut tree, false, |_, _, _| panic!("must not rebuild"));
// And back: a fresh branch mounts in the same slot.
conditional.set(&mut tree, true, build_then);
tree.render_frame(Size::new(200.0, 200.0));
let children = tree.children(root);
assert_eq!(children.len(), 5);
assert_eq!(children[0], before);
assert_eq!(children[4], after);
}
/// A region's content belongs to the scope that mounted the region.
///
/// Content is built from the command queue, where no scope is entered, so
/// without an owner of its own every row scope would be a child of the root
/// and would outlive the region — and its effects would keep running against
/// widgets that are gone.
#[test]
fn rows_die_with_the_scope_that_mounted_the_region() {
let (mut tree, root) = tree_with_root();
let owner = Scope::new();
let mut list: KeyedList<String, String> = owner.run(|| KeyedList::new(&mut tree, Some(root)));
let items: Vec<Signal<String>> = Default::default();
let items = std::cell::RefCell::new(items);
let build = |tree: &mut WidgetTree,
parent: Option<WidgetId>,
at: usize,
item: Signal<String>,
index: Signal<i64>| {
items.borrow_mut().push(item);
build_row(tree, parent, at, item, index)
};
// Reconciled outside `owner.run`, as the command drain does it.
let values: Vec<String> = ["a", "b"].map(String::from).into();
list.reconcile(&mut tree, &values, |_, item| item.clone(), build);
assert!(
items.borrow().iter().all(|item| item.is_alive()),
"the rows are live while their region's owner is"
);
owner.dispose();
assert!(
items.borrow().iter().all(|item| !item.is_alive()),
"and went with it, rather than outliving it in the root scope"
);
}
/// An update that arrives after the region was removed does nothing.
///
/// Regions are updated through the command queue, so an enclosing region can
/// retire this one — or its component can be unmounted — between the effect
/// queueing an update and the drain running it.
#[test]
fn a_removed_region_ignores_an_update_queued_before_it_went() {
let (mut tree, root) = tree_with_root();
let mut conditional = Conditional::new(&mut tree, Some(root));
let mut list: KeyedList<String, String> = KeyedList::new(&mut tree, Some(root));
// Exactly what an enclosing region does when it retires a row.
for region in [conditional.extent(), list.extent()] {
let Extent::Region { lead, trail } = region else {
panic!("a region's extent is a run");
};
tree.remove(lead);
tree.remove(trail);
}
conditional.set(&mut tree, true, |_, _, _| {
panic!("a removed region builds nothing")
});
let values: Vec<String> = ["a"].map(String::from).into();
list.reconcile(
&mut tree,
&values,
|_, item| item.clone(),
|_, _, _, _, _| panic!("a removed region builds nothing"),
);
assert!(list.is_empty());
assert!(tree.children(root).is_empty(), "and mounted nothing");
}
/// A region can be another region's row, which is what the two anchors buy:
/// the outer list places, reorders, and retires a body whose widget count it
/// does not know and which changes after placement.
#[test]
fn a_row_can_be_a_nested_region() {
let (mut tree, root) = tree_with_root();
let before = tree.insert(Container::new(), row_style(), Some(root));
let mut list: KeyedList<String, String> = KeyedList::new(&mut tree, Some(root));
let after = tree.insert(Container::new(), row_style(), Some(root));
// Each row is a `Conditional` that starts empty; the inner regions are
// kept alive by the test, standing in for the row scope that holds them
// in generated code.
let inner: std::rc::Rc<std::cell::RefCell<Vec<Conditional>>> = Default::default();
let build = |tree: &mut WidgetTree,
parent: Option<WidgetId>,
at: usize,
_item: Signal<String>,
_index: Signal<i64>| {
let region = Conditional::new(tree, parent);
let extent = region.extent();
place(tree, parent, extent, at);
inner.borrow_mut().push(region);
extent
};
let items: Vec<String> = ["a", "b"].map(String::from).into();
list.reconcile(&mut tree, &items, |_, item| item.clone(), build);
// before, list-lead, [a-lead, a-trail], [b-lead, b-trail], list-trail, after
assert_eq!(tree.children(root).len(), 8);
assert_eq!(tree.children(root)[0], before);
assert_eq!(tree.children(root)[7], after);
// The inner regions gain content *after* the outer list placed them —
// the case a captured widget list could not survive.
for region in inner.borrow_mut().iter_mut() {
region.set(&mut tree, true, |tree, parent, at| {
let id = tree.insert_at(Container::new(), row_style(), parent.expect("parent"), at);
Some(Extent::Single(id))
});
}
let children = tree.children(root).to_vec();
assert_eq!(children.len(), 10, "each row grew by one widget");
// Each row's content sits inside its own anchors, and the rows stay
// contiguous between the list's.
assert_eq!(children[0], before);
assert_eq!(children[9], after);
// Reordering moves whole runs: row `b` and its content go first, as a
// block, and the surrounding siblings do not move.
let row_a: Vec<WidgetId> = children[2..5].to_vec();
let row_b: Vec<WidgetId> = children[5..8].to_vec();
let items: Vec<String> = ["b", "a"].map(String::from).into();
list.reconcile(&mut tree, &items, |_, item| item.clone(), build);
let children = tree.children(root).to_vec();
assert_eq!(children.len(), 10, "a reorder mounts nothing new");
assert_eq!(children[0], before);
assert_eq!(children[9], after);
assert_eq!(children[2..5], row_b[..], "`b`'s whole run moved first");
assert_eq!(children[5..8], row_a[..], "`a`'s whole run followed");
// Retiring a row takes its whole run with it.
let items: Vec<String> = ["a"].map(String::from).into();
list.reconcile(&mut tree, &items, |_, item| item.clone(), build);
let children = tree.children(root).to_vec();
assert_eq!(children.len(), 7, "`b`'s three widgets all went");
for id in row_b {
assert!(!children.contains(&id), "no widget of `b` survived");
}
}