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);
}