use std::cell::{Cell, RefCell};
use std::rc::Rc;
use crate::base::{Point, Size};
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{flush_effects, run_due_timers, Signal};
use crate::ui::{BufferCanvas, Element, UiTree};
use crate::widgets::itest_util::{mount_widget, render};
use crate::widgets::{Feed, FeedItem, FeedState, SyncSpec};
#[derive(Clone)]
struct Msg {
id: String,
rev: u64,
hidden: bool,
text: String,
}
fn msg(id: &str, text: &str) -> Msg {
Msg {
id: id.to_string(),
rev: 0,
hidden: false,
text: text.to_string(),
}
}
fn settle(tree: &mut UiTree, size: Size) -> BufferCanvas {
flush_effects();
tree.layout();
let _ = render(tree, size);
run_due_timers(std::time::Instant::now());
flush_effects();
tree.layout();
render(tree, size)
}
type SyncedMount = (
crate::reactive::RootScope,
UiTree,
Signal<Vec<Msg>>,
FeedState,
Rc<Cell<usize>>,
);
type StateHolder = Rc<RefCell<Option<(Signal<Vec<Msg>>, FeedState)>>>;
fn mount_synced(size: Size) -> SyncedMount {
let renders = Rc::new(Cell::new(0usize));
let holder: StateHolder = Rc::new(RefCell::new(None));
let (h, r) = (holder.clone(), renders.clone());
let (root, mut tree) = mount_widget(size, move |cx| {
let items: Signal<Vec<Msg>> = cx.signal(Vec::new());
let feed = FeedState::new(cx);
feed.sync(
cx,
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((items, 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 (items, feed) = holder.borrow().clone().expect("state captured");
let _ = settle(&mut tree, size);
(root, tree, items, feed, renders)
}
fn mount_reference(size: Size) -> (crate::reactive::RootScope, UiTree, FeedState) {
let holder: Rc<RefCell<Option<FeedState>>> = Rc::new(RefCell::new(None));
let h = holder.clone();
let (root, tree) = mount_widget(size, move |cx| {
let feed = FeedState::new(cx);
*h.borrow_mut() = Some(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 feed = holder.borrow().clone().expect("state captured");
(root, tree, feed)
}
fn assert_same_pixels(a: &BufferCanvas, b: &BufferCanvas, size: Size, label: &str) {
for y in 0..size.h {
for x in 0..size.w {
assert_eq!(
a.cell(Point::new(x, y)),
b.cell(Point::new(x, y)),
"{label}: divergence at ({x},{y}):\nsynced: {:?}\nhand: {:?}",
a.row_text(y),
b.row_text(y)
);
}
}
}
fn assert_parity(label: &str, list: Vec<Msg>) {
let size = Size::new(26, 14);
let (root_a, mut tree_a, items, _feed, _r) = mount_synced(size);
items.set(list.clone());
let canvas_a = settle(&mut tree_a, size);
let (root_b, mut tree_b, reference) = mount_reference(size);
for m in list.iter().filter(|m| !m.hidden) {
reference.push(m.id.clone(), FeedItem::text(m.text.clone()));
}
let canvas_b = settle(&mut tree_b, size);
assert_same_pixels(&canvas_a, &canvas_b, size, label);
root_a.dispose();
root_b.dispose();
}
#[test]
fn append_only_folds_take_the_push_path_and_never_rebuild() {
let size = Size::new(24, 12);
let (root, mut tree, items, feed, renders) = mount_synced(size);
items.set(vec![msg("a", "alpha"), msg("b", "beta")]);
flush_effects();
assert_eq!(renders.get(), 2, "initial fill renders each item once");
assert_eq!(feed.len(), 2);
for i in 0..10 {
items.update(|v| v.push(msg(&format!("k{i}"), &format!("line {i}"))));
flush_effects();
}
assert_eq!(
renders.get(),
12,
"append-only fold must never re-render shown items (no rebuild)"
);
assert_eq!(feed.len(), 12);
let canvas = settle(&mut tree, size);
assert!(canvas.row_text(0).contains("alpha"));
root.dispose();
}
#[test]
fn fingerprint_change_updates_in_place_without_rebuild() {
let size = Size::new(24, 12);
let (root, mut tree, items, feed, renders) = mount_synced(size);
items.set(vec![msg("a", "alpha"), msg("b", "beta"), msg("c", "gamma")]);
flush_effects();
assert_eq!(renders.get(), 3);
items.update(|v| {
v[1].rev = 1;
v[1].text = "BETA2".into();
});
flush_effects();
assert_eq!(renders.get(), 4, "one changed fingerprint = one render");
assert_eq!(feed.len(), 3, "update in place, not append");
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("BETA2")), "{dump:#?}");
assert!(!dump.iter().any(|r| r.contains("beta")), "old text gone");
root.dispose();
}
#[test]
fn mid_list_insert_rebuilds_exactly_once() {
let size = Size::new(24, 12);
let (root, _tree, items, feed, renders) = mount_synced(size);
items.set(vec![msg("a", "alpha"), msg("c", "gamma")]);
flush_effects();
assert_eq!(renders.get(), 2);
items.update(|v| v.insert(1, msg("b", "beta")));
flush_effects();
assert_eq!(renders.get(), 5, "mid-list insert = one whole rebuild");
assert_eq!(feed.len(), 3);
root.dispose();
}
#[test]
fn visibility_flips_mid_list_rebuild_and_tail_flips_append() {
let size = Size::new(24, 12);
let (root, _tree, items, feed, renders) = mount_synced(size);
items.set(vec![msg("a", "alpha"), msg("b", "beta"), msg("c", "gamma")]);
flush_effects();
assert_eq!(renders.get(), 3);
items.update(|v| v[1].hidden = true);
flush_effects();
assert_eq!(renders.get(), 5, "mid-list hide rebuilds the window");
assert_eq!(feed.len(), 2);
items.update(|v| {
let mut d = msg("d", "delta");
d.hidden = true;
v.push(d);
});
flush_effects();
assert_eq!(renders.get(), 5, "hidden tail item renders nothing");
items.update(|v| v[3].hidden = false);
flush_effects();
assert_eq!(renders.get(), 6, "tail-only visibility flip appends");
assert_eq!(feed.len(), 3);
root.dispose();
}
#[test]
fn shrink_and_reorder_take_the_rebuild_path() {
let size = Size::new(24, 12);
let (root, _tree, items, feed, renders) = mount_synced(size);
items.set(vec![msg("a", "alpha"), msg("b", "beta"), msg("c", "gamma")]);
flush_effects();
assert_eq!(renders.get(), 3);
items.update(|v| {
v.pop();
});
flush_effects();
assert_eq!(renders.get(), 5, "shrink rebuilds (2 items re-render)");
assert_eq!(feed.len(), 2);
items.update(|v| v.swap(0, 1));
flush_effects();
assert_eq!(renders.get(), 7, "reorder rebuilds");
assert_eq!(feed.len(), 2);
root.dispose();
}
#[test]
fn parity_reorder_midlist_update_burst_append_full_replace() {
assert_parity(
"reorder",
vec![msg("b", "second first"), msg("a", "first second")],
);
let mut updated = vec![msg("a", "alpha"), msg("b", "changed body"), msg("c", "g")];
updated[1].rev = 3;
assert_parity("mid-list update", updated);
let size = Size::new(26, 14);
let (root_a, mut tree_a, items, _f, _r) = mount_synced(size);
items.set(vec![msg("a", "alpha")]);
flush_effects();
items.update(|v| {
for i in 0..6 {
v.push(msg(&format!("b{i}"), &format!("burst {i}")));
}
});
let canvas_a = settle(&mut tree_a, size);
let (root_b, mut tree_b, reference) = mount_reference(size);
reference.push("a", FeedItem::text("alpha"));
for i in 0..6 {
reference.push(format!("b{i}"), FeedItem::text(format!("burst {i}")));
}
let canvas_b = settle(&mut tree_b, size);
assert_same_pixels(&canvas_a, &canvas_b, size, "burst append");
root_a.dispose();
root_b.dispose();
let size = Size::new(26, 14);
let (root_a, mut tree_a, items, _f, _r) = mount_synced(size);
items.set(vec![msg("a", "alpha"), msg("b", "beta")]);
flush_effects();
items.set(vec![msg("x", "new one"), msg("y", "new two")]);
let canvas_a = settle(&mut tree_a, size);
let (root_b, mut tree_b, reference) = mount_reference(size);
reference.push("x", FeedItem::text("new one"));
reference.push("y", FeedItem::text("new two"));
let canvas_b = settle(&mut tree_b, size);
assert_same_pixels(&canvas_a, &canvas_b, size, "full replace");
root_a.dispose();
root_b.dispose();
}
#[test]
fn hidden_items_never_reach_the_feed_and_parity_holds_with_filter() {
let mut list = vec![msg("a", "alpha"), msg("b", "beta"), msg("c", "gamma")];
list[1].hidden = true;
assert_parity("hidden mid-list", list);
}
#[test]
fn foreign_push_between_drains_self_heals_with_a_rebuild() {
let size = Size::new(24, 12);
let (root, mut tree, items, feed, renders) = mount_synced(size);
items.set(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");
items.update(|v| v.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:#?}"
);
items.update(|v| v.push(msg("d", "delta")));
flush_effects();
assert_eq!(renders.get(), 6, "fast paths resume after the heal");
assert_eq!(feed.len(), 4);
root.dispose();
}
#[test]
fn foreign_push_of_a_future_source_key_heals_to_source_order() {
let size = Size::new(24, 12);
let (root, mut tree, items, feed, _renders) = mount_synced(size);
items.set(vec![msg("a", "alpha")]);
flush_effects();
feed.push("c", FeedItem::text("premature gamma"));
items.update(|v| {
v.push(msg("b", "beta"));
v.push(msg("c", "gamma"));
});
let canvas = settle(&mut tree, size);
assert_eq!(feed.len(), 3);
let rows: Vec<i32> = ["a", "b", "c"]
.iter()
.map(|k| feed.row_of(k).expect("mirrored key"))
.collect();
assert!(
rows[0] < rows[1] && rows[1] < rows[2],
"feed order equals source order after the heal: {rows:?}"
);
let dump: Vec<String> = (0..size.h).map(|y| canvas.row_text(y)).collect();
assert!(
dump.iter().any(|r| r.contains("gamma")),
"the source's render won, not the foreign content: {dump:#?}"
);
assert!(
!dump.iter().any(|r| r.contains("premature")),
"foreign content evicted: {dump:#?}"
);
root.dispose();
}
#[test]
#[ignore]
fn perf_sync_burst_1k_into_10k() {
let size = Size::new(40, 12);
let m = crate::testing::time_median("sync burst 1k into 10k", 1, 5, 1, |_| {
let (root, mut tree, items, feed, renders) = mount_synced(size);
items.set(
(0..10_000)
.map(|i| msg(&format!("k{i}"), "seed row"))
.collect(),
);
flush_effects();
let _ = settle(&mut tree, size);
assert_eq!(renders.get(), 10_000);
let start = std::time::Instant::now();
items.update(|v| {
for i in 0..1_000 {
v.push(msg(&format!("b{i}"), "burst row"));
}
});
flush_effects();
eprintln!(" burst fold alone: {:?}", start.elapsed());
assert_eq!(renders.get(), 11_000, "burst renders only the 1k new items");
assert_eq!(feed.len(), 11_000);
root.dispose();
});
eprintln!("{}", m.report());
if !cfg!(debug_assertions) {
m.assert_under(std::time::Duration::from_secs(3));
}
}