use super::*;
struct Fold {
items: Vec<Msg>,
stats: u64,
}
struct Convo {
items: Vec<Msg>,
}
type FoldMount = (
crate::reactive::RootScope,
UiTree,
Signal<Fold>,
FeedState,
Rc<Cell<usize>>,
);
type FoldHolder = Rc<RefCell<Option<(Signal<Fold>, FeedState)>>>;
fn mount_fold(size: Size) -> FoldMount {
let renders = Rc::new(Cell::new(0usize));
let holder: FoldHolder = Rc::new(RefCell::new(None));
let (h, r) = (holder.clone(), renders.clone());
let (root, mut tree) = mount_widget(size, move |cx| {
let fold: Signal<Fold> = cx.signal(Fold {
items: Vec::new(),
stats: 0,
});
let feed = FeedState::new(cx);
feed.sync_with(
cx,
move |read| fold.with(|f| read(&f.items)),
SyncSpec::new(
|m: &Msg| m.id.to_string(),
|m| m.rev,
move |m| {
r.set(r.get() + 1);
FeedItem::text(m.text.clone())
},
)
.visible(|m| !m.hidden),
);
*h.borrow_mut() = Some((fold, feed.clone()));
Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0)),
)
.child(Feed::new(&feed).view(cx))
.build()
});
let (fold, feed) = holder.borrow_mut().take().expect("state captured");
let _ = settle(&mut tree, size);
(root, tree, fold, feed, renders)
}
#[test]
fn fold_shaped_source_syncs_and_stats_only_writes_render_nothing() {
let size = Size::new(24, 12);
let (root, mut tree, fold, feed, renders) = mount_fold(size);
fold.update(|f| {
f.items = vec![msg("a", "alpha"), msg("b", "beta")];
f.stats += 1;
});
flush_effects();
assert_eq!(renders.get(), 2, "initial fill renders each item once");
assert_eq!(feed.len(), 2);
for _ in 0..5 {
fold.update(|f| f.stats += 1);
flush_effects();
}
assert_eq!(
renders.get(),
2,
"stats-only writes must not re-render items (fingerprint path)"
);
assert_eq!(feed.len(), 2, "no rebuild either");
fold.update(|f| {
f.stats += 1;
f.items.push(msg("c", "gamma"));
});
flush_effects();
assert_eq!(renders.get(), 3, "append renders only the new item");
assert_eq!(feed.len(), 3);
fold.update(|f| {
f.items[1].rev = 1;
f.items[1].text = "BETA2".into();
});
flush_effects();
assert_eq!(renders.get(), 4, "one changed fingerprint = one render");
let canvas = settle(&mut tree, size);
let dump: Vec<String> = (0..size.h).map(|y| canvas.row_text(y)).collect();
assert!(dump.iter().any(|r| r.contains("alpha")), "{dump:#?}");
assert!(dump.iter().any(|r| r.contains("BETA2")), "{dump:#?}");
assert!(dump.iter().any(|r| r.contains("gamma")), "{dump:#?}");
root.dispose();
}
#[test]
fn focus_switched_nested_source_rebuilds_on_focus_and_stays_fast_within() {
let size = Size::new(24, 12);
let renders = Rc::new(Cell::new(0usize));
type Held = (Signal<Vec<Convo>>, Signal<usize>, FeedState);
let holder: Rc<RefCell<Option<Held>>> = Rc::new(RefCell::new(None));
let (h, r) = (holder.clone(), renders.clone());
let (root, _tree) = mount_widget(size, move |cx| {
let convos: Signal<Vec<Convo>> = cx.signal(vec![
Convo {
items: vec![msg("c0-a", "zero alpha")],
},
Convo {
items: vec![msg("c1-x", "one ex"), msg("c1-y", "one why")],
},
]);
let focus: Signal<usize> = cx.signal(0usize);
let feed = FeedState::new(cx);
feed.sync_with(
cx,
move |read| {
let at = focus.get();
convos.with(|cs| read(&cs[at].items));
},
SyncSpec::new(
|m: &Msg| m.id.to_string(),
|m| m.rev,
move |m| {
r.set(r.get() + 1);
FeedItem::text(m.text.clone())
},
),
);
*h.borrow_mut() = Some((convos, focus, feed.clone()));
Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0)),
)
.child(Feed::new(&feed).view(cx))
.build()
});
let (convos, focus, feed) = holder.borrow_mut().take().expect("state captured");
flush_effects();
assert_eq!(renders.get(), 1, "focused convo 0 renders its one item");
assert_eq!(feed.len(), 1);
convos.update(|cs| cs[0].items.push(msg("c0-b", "zero beta")));
flush_effects();
assert_eq!(renders.get(), 2, "in-focus append is O(1)");
assert_eq!(feed.len(), 2);
focus.set(1);
flush_effects();
assert_eq!(renders.get(), 4, "focus switch rebuilds to the other convo");
assert_eq!(feed.len(), 2);
assert!(feed.row_of("c1-x").is_some(), "convo 1 items shown");
assert!(feed.row_of("c0-a").is_none(), "convo 0 items gone");
convos.update(|cs| cs[0].items.push(msg("c0-c", "zero gamma")));
flush_effects();
assert_eq!(renders.get(), 4, "unfocused-convo writes render nothing");
convos.update(|cs| cs[1].items.push(msg("c1-z", "one zed")));
flush_effects();
assert_eq!(renders.get(), 5, "post-switch append is O(1)");
assert_eq!(feed.len(), 3);
root.dispose();
}
#[test]
fn self_heal_still_fires_through_the_borrow_door() {
let size = Size::new(24, 12);
let (root, mut tree, fold, feed, renders) = mount_fold(size);
fold.update(|f| f.items = vec![msg("a", "alpha"), msg("b", "beta")]);
flush_effects();
assert_eq!(renders.get(), 2);
feed.push("stray", FeedItem::text("stray row"));
assert_eq!(feed.len(), 3, "precondition: the stray landed");
fold.update(|f| f.items.push(msg("c", "gamma")));
flush_effects();
assert_eq!(feed.len(), 3, "self-heal rebuilt: 3 mirrored, 0 stray");
assert_eq!(
renders.get(),
5,
"the heal is one full rebuild (2 + 3 renders)"
);
let canvas = settle(&mut tree, size);
let dump: Vec<String> = (0..size.h).map(|y| canvas.row_text(y)).collect();
assert!(
!dump.iter().any(|r| r.contains("stray row")),
"stray write evicted at the next drain: {dump:#?}"
);
fold.update(|f| f.items.push(msg("d", "delta")));
flush_effects();
assert_eq!(renders.get(), 6, "fast paths resume after the heal");
assert_eq!(feed.len(), 4);
root.dispose();
}