use std::cell::Cell; use std::rc::Rc; use super::*; fn counter() -> (Rc>, impl Fn() + Clone + 'static) { let count = Rc::new(Cell::new(0)); let bump = { let count = count.clone(); move || count.set(count.get() + 1) }; (count, bump) } #[test] fn effect_runs_immediately_and_on_change() { let signal = Signal::new(1); let (runs, bump) = counter(); let _effect = Effect::new(move || { signal.get(); bump(); }); assert_eq!(runs.get(), 1, "effects run once at creation"); signal.set(2); assert_eq!(runs.get(), 1, "effects are deferred until flush"); flush_effects(); assert_eq!(runs.get(), 2); } #[test] fn equal_write_does_not_notify() { let signal = Signal::new(5); let (runs, bump) = counter(); let _effect = Effect::new(move || { signal.get(); bump(); }); signal.set(5); assert!(!has_pending_effects()); flush_effects(); assert_eq!(runs.get(), 1); } #[test] fn multiple_writes_one_run() { let signal = Signal::new(0); let (runs, bump) = counter(); let _effect = Effect::new(move || { signal.get(); bump(); }); signal.set(1); signal.set(2); signal.set(3); flush_effects(); assert_eq!(runs.get(), 2, "three writes, one flush, one re-run"); assert_eq!(signal.get_untracked(), 3); } #[test] fn diamond_dependency_runs_effect_once_consistently() { let a = Signal::new(1); let b = Memo::new(move || a.get() * 10); let c = Memo::new(move || a.get() + 100); let (runs, bump) = counter(); let seen = Rc::new(Cell::new((0, 0))); let seen2 = seen.clone(); let _effect = Effect::new(move || { seen2.set((b.get(), c.get())); bump(); }); assert_eq!(runs.get(), 1); assert_eq!(seen.get(), (10, 101)); a.set(2); flush_effects(); assert_eq!( runs.get(), 2, "diamond: effect ran exactly once for one write" ); assert_eq!(seen.get(), (20, 102), "both branches were consistent"); } #[test] fn memo_equality_gates_downstream() { let a = Signal::new(1); // Parity only changes when a crosses even/odd. let parity = Memo::new(move || a.get() % 2); let (runs, bump) = counter(); let _effect = Effect::new(move || { parity.get(); bump(); }); assert_eq!(runs.get(), 1); a.set(3); // parity unchanged (1 -> 1) flush_effects(); assert_eq!( runs.get(), 1, "memo value unchanged, effect must not re-run" ); a.set(4); // parity changed (1 -> 0) flush_effects(); assert_eq!(runs.get(), 2); } #[test] fn memo_is_lazy_outside_effects() { let a = Signal::new(1); let (computes, bump) = counter(); let m = Memo::new(move || { bump(); a.get() * 2 }); assert_eq!(computes.get(), 1, "memos compute once eagerly at creation"); a.set(2); a.set(3); assert_eq!(computes.get(), 1, "writes alone do not recompute a memo"); assert_eq!(m.get(), 6, "reading recomputes on demand"); assert_eq!(computes.get(), 2, "one recompute despite two writes"); assert_eq!(m.get(), 6); assert_eq!(computes.get(), 2, "clean memo reads are cached"); } #[test] fn conditional_dependencies_retrack() { let flag = Signal::new(true); let a = Signal::new(0); let b = Signal::new(0); let (runs, bump) = counter(); let _effect = Effect::new(move || { if flag.get() { a.get(); } else { b.get(); } bump(); }); assert_eq!(runs.get(), 1); flag.set(false); flush_effects(); assert_eq!(runs.get(), 2); a.set(7); // no longer a dependency flush_effects(); assert_eq!(runs.get(), 2, "stale branch dependency must be dropped"); b.set(7); // current dependency flush_effects(); assert_eq!(runs.get(), 3); } #[test] fn effect_writing_signals_converges() { let a = Signal::new(0); let b = Signal::new(0); // Effect chain: a -> b (a's effect writes b; b's effect just observes). let _forward = Effect::new(move || { let v = a.get(); b.set(v * 2); }); let (runs, bump) = counter(); let seen = Rc::new(Cell::new(0)); let seen2 = seen.clone(); let _observe = Effect::new(move || { seen2.set(b.get()); bump(); }); a.set(21); flush_effects(); assert_eq!(seen.get(), 42, "flush keeps draining until quiescent"); assert!(!has_pending_effects()); assert_eq!(runs.get(), 2, "observer ran once at creation, once for b"); } #[test] fn dispose_effect_stops_reruns() { let a = Signal::new(0); let (runs, bump) = counter(); let effect = Effect::new(move || { a.get(); bump(); }); effect.dispose(); a.set(1); flush_effects(); assert_eq!(runs.get(), 1); } #[test] fn scope_disposal_tears_down_nodes() { let a = Signal::new(0); let (runs, bump) = counter(); let scope = Scope::new(); let inner = scope.run(|| { let inner = Signal::new(10); let _effect = Effect::new(move || { a.get(); inner.get(); bump(); }); inner }); assert_eq!(runs.get(), 1); assert!(inner.is_alive()); scope.dispose(); assert!(!inner.is_alive()); assert_eq!(inner.try_get(), None); a.set(5); flush_effects(); assert_eq!(runs.get(), 1, "scoped effect must not survive disposal"); // Writing the disposed signal is a defined no-op. inner.set(99); } #[test] fn signals_created_inside_effects_work() { let trigger = Signal::new(0); let created: Rc>>> = Rc::new(Cell::new(None)); let created2 = created.clone(); let _effect = Effect::new(move || { let s = Signal::new(trigger.get() * 2); created2.set(Some(s)); }); trigger.set(4); flush_effects(); let s = created.get().expect("effect ran"); assert_eq!(s.get_untracked(), 8); } #[test] fn update_mutates_in_place() { let v = Signal::new(vec![1, 2]); let (runs, bump) = counter(); let _effect = Effect::new(move || { v.with(|v| v.len()); bump(); }); v.update(|v| v.push(3)); flush_effects(); assert_eq!(runs.get(), 2); assert_eq!(v.with_untracked(|v| v.clone()), vec![1, 2, 3]); } #[test] fn read_signal_tracks_but_cannot_write() { let owner = Signal::new(1); let reader = owner.read_only(); let (runs, bump) = counter(); let _effect = Effect::new(move || { reader.get(); bump(); }); assert_eq!(runs.get(), 1); owner.set(2); flush_effects(); assert_eq!(runs.get(), 2, "reads through ReadSignal track"); assert_eq!(reader.get_untracked(), 2); assert_eq!(reader.try_get(), Some(2)); owner.dispose(); assert_eq!(reader.try_get(), None); }