tests.rs
raw
use std::cell::Cell;
use std::rc::Rc;
use super::*;
fn counter() -> (Rc<Cell<usize>>, 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<Cell<Option<Signal<i32>>>> = 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);
}