use super::*;
use std::cell::RefCell;
use std::rc::Rc;
fn fresh() {
__reset_for_tests();
}
#[test]
fn signal_returns_initial_value() {
fresh();
let (read, _write) = signal(42_i32);
assert_eq!(read.get(), 42);
}
#[test]
fn write_signal_updates_value() {
fresh();
let (read, write) = signal(0_i32);
write.set(7);
assert_eq!(read.get(), 7);
}
#[test]
fn rw_signal_split_round_trip() {
fresh();
let rw = RwSignal::new(0_i32);
rw.set(5);
let (r, w) = rw.split();
assert_eq!(r.get(), 5);
w.set(9);
assert_eq!(rw.get(), 9);
}
#[test]
fn with_borrows_without_clone() {
fresh();
let (read, write) = signal(vec![1, 2, 3]);
let sum = read.with(|v| v.iter().sum::<i32>());
assert_eq!(sum, 6);
write.update(|v| v.push(4));
assert_eq!(read.with(|v| v.len()), 4);
}
#[test]
fn signal_handles_are_copy_and_aliasable() {
fresh();
let (read, _write) = signal(String::from("hello"));
let a = read;
let b = read;
assert_eq!(a.get(), "hello");
assert_eq!(b.get(), "hello");
}
#[test]
fn effect_runs_once_immediately() {
fresh();
let counter = Rc::new(RefCell::new(0));
let c = counter.clone();
effect(move || *c.borrow_mut() += 1);
assert_eq!(*counter.borrow(), 1);
}
#[test]
fn effect_reruns_on_dep_change() {
fresh();
let (count, set_count) = signal(0_i32);
let log: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let log_clone = log.clone();
effect(move || {
log_clone.borrow_mut().push(count.get());
});
set_count.set(1);
flush();
set_count.set(2);
flush();
assert_eq!(*log.borrow(), vec![0, 1, 2]);
}
#[test]
fn effect_only_reruns_for_tracked_deps() {
fresh();
let (tracked, set_tracked) = signal(0_i32);
let (untracked, set_untracked) = signal(100_i32);
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
effect(move || {
let _ = tracked.get();
let _ = untracked.get_untracked();
*runs_clone.borrow_mut() += 1;
});
assert_eq!(*runs.borrow(), 1);
set_untracked.set(999);
flush();
assert_eq!(*runs.borrow(), 1, "untracked write must not re-run effect");
set_tracked.set(1);
flush();
assert_eq!(*runs.borrow(), 2, "tracked write must re-run effect");
}
#[test]
fn effect_dynamic_deps_change_between_runs() {
fresh();
let (toggle, set_toggle) = signal(false);
let (a, set_a) = signal(0_i32);
let (b, set_b) = signal(0_i32);
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
effect(move || {
if toggle.get() {
let _ = a.get();
} else {
let _ = b.get();
}
*runs_clone.borrow_mut() += 1;
});
assert_eq!(*runs.borrow(), 1);
set_a.set(1);
flush();
assert_eq!(*runs.borrow(), 1);
set_b.set(1);
flush();
assert_eq!(*runs.borrow(), 2);
set_toggle.set(true);
flush();
assert_eq!(*runs.borrow(), 3);
set_b.set(2);
flush();
assert_eq!(*runs.borrow(), 3);
set_a.set(2);
flush();
assert_eq!(*runs.borrow(), 4);
}
#[test]
fn multiple_writes_coalesce_into_one_rerun() {
fresh();
let (a, set_a) = signal(0_i32);
let (b, set_b) = signal(0_i32);
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
effect(move || {
let _ = a.get();
let _ = b.get();
*runs_clone.borrow_mut() += 1;
});
assert_eq!(*runs.borrow(), 1);
set_a.set(1);
set_b.set(2);
flush();
assert_eq!(
*runs.borrow(),
2,
"two writes must produce exactly one re-run"
);
}
#[test]
fn signals_written_during_flush_propagate_in_same_flush() {
fresh();
let (a, set_a) = signal(0_i32);
let (b, set_b) = signal(0_i32);
let cascade_runs = Rc::new(RefCell::new(0));
let cascade_clone = cascade_runs.clone();
effect(move || {
let v = a.get();
set_b.set(v * 10);
});
effect(move || {
let _ = b.get();
*cascade_clone.borrow_mut() += 1;
});
set_a.set(3);
flush();
assert_eq!(b.get_untracked(), 30);
assert!(
*cascade_runs.borrow() >= 2,
"cascading write must re-trigger downstream effect"
);
}
#[test]
fn dispose_owner_frees_nested_signals() {
fresh();
let owner = Owner::new(None);
let (read, _write) = owner.with(|| signal(123_i32));
assert_eq!(read.get(), 123);
owner.dispose();
owner.dispose();
}
#[test]
fn dispose_cascades_to_children() {
fresh();
let parent = Owner::new(None);
let mut leaf_signals = Vec::new();
let child = parent.with(|| {
let c = Owner::new(None);
c.with(|| {
let (r, _w) = signal(0_u32);
leaf_signals.push(r);
});
c
});
parent.dispose();
let alive = with_runtime(|rt| {
(
rt.owners.contains_key(parent),
rt.owners.contains_key(child),
)
});
assert_eq!(alive, (false, false));
}
#[test]
fn on_cleanup_fires_lifo() {
fresh();
let owner = Owner::new(None);
let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
owner.with(|| {
let l = log.clone();
on_cleanup(move || l.borrow_mut().push("first"));
let l = log.clone();
on_cleanup(move || l.borrow_mut().push("second"));
let l = log.clone();
on_cleanup(move || l.borrow_mut().push("third"));
});
owner.dispose();
assert_eq!(*log.borrow(), vec!["third", "second", "first"]);
}
#[test]
fn disposing_owner_removes_its_effects_from_pending() {
fresh();
let owner = Owner::new(None);
let (count, set_count) = signal(0_i32);
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
owner.with(|| {
effect(move || {
let _ = count.get();
*runs_clone.borrow_mut() += 1;
});
});
assert_eq!(*runs.borrow(), 1);
set_count.set(1);
owner.dispose();
flush();
assert_eq!(*runs.borrow(), 1);
}
#[test]
fn stored_value_read_write() {
fresh();
let sv = StoredValue::new(vec![1_i32, 2, 3]);
assert_eq!(sv.with(|v| v.iter().sum::<i32>()), 6);
sv.update(|v| v.push(4));
assert_eq!(sv.with(|v| v.len()), 4);
}
#[test]
fn stored_value_does_not_trigger_effects() {
fresh();
let sv = StoredValue::new(0_i32);
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
effect(move || {
let _ = sv.with(|v| *v);
*runs_clone.borrow_mut() += 1;
});
assert_eq!(*runs.borrow(), 1);
sv.set(99);
flush();
assert_eq!(
*runs.borrow(),
1,
"StoredValue writes must not trigger reactivity"
);
}
#[test]
fn stored_value_disposed_with_owner() {
fresh();
let owner = Owner::new(None);
let sv = owner.with(|| StoredValue::new(123_i32));
assert_eq!(sv.get(), 123);
owner.dispose();
let leftover = with_runtime(|rt| rt.owners.contains_key(owner));
assert!(!leftover);
}
#[test]
fn computed_caches_initial_value() {
fresh();
let (count, _set) = signal(3_i32);
let doubled = computed(move || count.get() * 2);
assert_eq!(doubled.get(), 6);
}
#[test]
fn computed_recomputes_on_source_change() {
fresh();
let (count, set_count) = signal(0_i32);
let doubled = computed(move || count.get() * 2);
assert_eq!(doubled.get(), 0);
set_count.set(5);
flush();
assert_eq!(doubled.get(), 10);
}
#[test]
fn computed_notifies_downstream_subscribers() {
fresh();
let (count, set_count) = signal(0_i32);
let doubled = computed(move || count.get() * 2);
let observed: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let obs_clone = observed.clone();
effect(move || obs_clone.borrow_mut().push(doubled.get()));
set_count.set(3);
flush();
set_count.set(7);
flush();
assert_eq!(*observed.borrow(), vec![0, 6, 14]);
}
#[test]
fn on_mount_callback_inside_effect_does_not_leak_subscription_to_outer() {
use super::component::{flush_mounts, on_mount};
fresh();
let (effect_dep, set_effect_dep) = signal(0_i32);
let (mount_src, set_mount_src) = signal(0_i32);
let outer_runs = Rc::new(RefCell::new(0));
let outer_runs_clone = outer_runs.clone();
effect(move || {
let _ = effect_dep.get();
*outer_runs_clone.borrow_mut() += 1;
let owner = super::Owner::new(None);
owner.with(|| {
on_mount(move || {
let _ = mount_src.get();
});
});
flush_mounts();
});
assert_eq!(*outer_runs.borrow(), 1);
set_mount_src.set(1);
flush();
assert_eq!(*outer_runs.borrow(), 1);
set_effect_dep.set(1);
flush();
assert_eq!(*outer_runs.borrow(), 2);
}
#[test]
fn mount_component_body_inside_effect_does_not_leak_subscription_to_outer() {
use super::component::mount_component;
fresh();
let (effect_dep, set_effect_dep) = signal(0_i32);
let (body_src, set_body_src) = signal(0_i32);
let outer_runs = Rc::new(RefCell::new(0));
let outer_runs_clone = outer_runs.clone();
effect(move || {
let _ = effect_dep.get();
*outer_runs_clone.borrow_mut() += 1;
let (_owner, _value) = mount_component(0xdead_beef as *const (), || body_src.get());
});
assert_eq!(*outer_runs.borrow(), 1);
set_body_src.set(1);
flush();
assert_eq!(*outer_runs.borrow(), 1);
set_effect_dep.set(1);
flush();
assert_eq!(*outer_runs.borrow(), 2);
}
#[test]
fn computed_constructed_inside_effect_does_not_leak_subscription_to_outer() {
fresh();
let (effect_dep, set_effect_dep) = signal(0_i32);
let (computed_src, set_computed_src) = signal(0_i32);
let outer_runs = Rc::new(RefCell::new(0));
let outer_runs_clone = outer_runs.clone();
effect(move || {
let _ = effect_dep.get();
*outer_runs_clone.borrow_mut() += 1;
let _doubled = computed(move || computed_src.get() * 2);
});
assert_eq!(*outer_runs.borrow(), 1);
set_computed_src.set(99);
flush();
assert_eq!(*outer_runs.borrow(), 1);
set_effect_dep.set(1);
flush();
assert_eq!(*outer_runs.borrow(), 2);
}
#[test]
fn computed_does_not_notify_when_value_unchanged() {
fresh();
let (count, set_count) = signal(5_i32);
let bucket = computed(move || count.get() / 10);
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
effect(move || {
let _ = bucket.get();
*runs_clone.borrow_mut() += 1;
});
assert_eq!(*runs.borrow(), 1);
set_count.set(7);
flush();
assert_eq!(*runs.borrow(), 1);
set_count.set(15);
flush();
assert_eq!(*runs.borrow(), 2);
}
#[derive(Clone, Debug, PartialEq)]
struct Theme(&'static str);
#[test]
fn context_round_trip_in_same_owner() {
fresh();
let owner = Owner::new(None);
owner.with(|| {
provide_context(Theme("dark"));
assert_eq!(use_context::<Theme>(), Some(Theme("dark")));
});
}
#[test]
fn context_walks_parent_chain() {
fresh();
let parent = Owner::new(None);
let observed = Rc::new(RefCell::new(None::<Theme>));
parent.with(|| {
provide_context(Theme("from-parent"));
let child = Owner::new(None);
let observed_clone = observed.clone();
child.with(|| {
*observed_clone.borrow_mut() = use_context::<Theme>();
});
});
assert_eq!(*observed.borrow(), Some(Theme("from-parent")));
}
#[test]
fn context_descendant_shadows_ancestor() {
fresh();
let parent = Owner::new(None);
let observed = Rc::new(RefCell::new(None::<Theme>));
parent.with(|| {
provide_context(Theme("outer"));
let inner = Owner::new(None);
let observed_clone = observed.clone();
inner.with(|| {
provide_context(Theme("inner"));
*observed_clone.borrow_mut() = use_context::<Theme>();
});
});
assert_eq!(*observed.borrow(), Some(Theme("inner")));
}
#[test]
fn context_missing_returns_none() {
fresh();
let owner = Owner::new(None);
let observed = Rc::new(RefCell::new(Some(Theme("placeholder"))));
let obs_clone = observed.clone();
owner.with(|| {
*obs_clone.borrow_mut() = use_context::<Theme>();
});
assert_eq!(*observed.borrow(), None);
}
#[test]
fn with_context_borrows_without_clone() {
fresh();
let owner = Owner::new(None);
let observed = Rc::new(RefCell::new(0_usize));
let obs_clone = observed.clone();
owner.with(|| {
provide_context(vec![1_i32, 2, 3, 4]);
let len = with_context::<Vec<i32>, _>(|v| v.len()).unwrap();
*obs_clone.borrow_mut() = len;
});
assert_eq!(*observed.borrow(), 4);
}
fn dummy_component_a() {} fn dummy_component_b() {}
#[test]
fn mount_component_registers_fn_pointer() {
fresh();
let (owner, _) = mount_component(dummy_component_a as *const (), || 42_i32);
let registered = owners_for_fn(dummy_component_a as *const ());
assert_eq!(registered, vec![owner]);
}
#[test]
fn unmount_component_removes_registration_and_disposes() {
fresh();
let (owner, _) = mount_component(dummy_component_a as *const (), || ());
assert_eq!(owners_for_fn(dummy_component_a as *const ()).len(), 1);
unmount_component(owner);
assert_eq!(owners_for_fn(dummy_component_a as *const ()).len(), 0);
let alive = with_runtime(|rt| rt.owners.contains_key(owner));
assert!(!alive);
}
#[test]
fn mount_component_isolates_owner_state() {
fresh();
let (owner_a, sig_a) = mount_component(dummy_component_a as *const (), || {
let (r, _w) = signal(1_i32);
r
});
let (owner_b, sig_b) = mount_component(dummy_component_b as *const (), || {
let (r, _w) = signal(2_i32);
r
});
assert_eq!(sig_a.get(), 1);
assert_eq!(sig_b.get(), 2);
unmount_component(owner_a);
assert_eq!(sig_b.get(), 2);
unmount_component(owner_b);
}
#[test]
fn on_mount_fires_on_flush() {
fresh();
let owner = Owner::new(None);
let fired = Rc::new(RefCell::new(false));
let fired_clone = fired.clone();
owner.with(|| {
on_mount(move || *fired_clone.borrow_mut() = true);
});
assert!(!*fired.borrow());
flush_mounts();
assert!(*fired.borrow());
}
#[test]
fn on_mount_runs_in_registration_order() {
fresh();
let owner = Owner::new(None);
let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
owner.with(|| {
let l = log.clone();
on_mount(move || l.borrow_mut().push("first"));
let l = log.clone();
on_mount(move || l.borrow_mut().push("second"));
let l = log.clone();
on_mount(move || l.borrow_mut().push("third"));
});
flush_mounts();
assert_eq!(*log.borrow(), vec!["first", "second", "third"]);
}
#[test]
fn flush_mounts_is_idempotent() {
fresh();
let owner = Owner::new(None);
let count = Rc::new(RefCell::new(0));
let count_clone = count.clone();
owner.with(|| {
on_mount(move || *count_clone.borrow_mut() += 1);
});
flush_mounts();
assert_eq!(*count.borrow(), 1);
flush_mounts();
assert_eq!(*count.borrow(), 1, "second flush must be a no-op");
}
#[test]
fn flush_breaks_self_feedback_loop_with_warning() {
fresh();
let (count, set_count) = signal(0_i32);
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
effect(move || {
let v = count.get();
set_count.set(v + 1);
*runs_clone.borrow_mut() += 1;
});
set_count.set(1);
flush();
let runs_after = *runs.borrow();
assert!(
runs_after < 1000,
"flush must break feedback loops; got {runs_after} runs"
);
}
#[test]
fn effect_reading_and_writing_unrelated_signals_terminates() {
fresh();
let (a, _) = signal(0_i32);
let (_b, set_b) = signal(0_i32);
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
effect(move || {
let v = a.get();
set_b.set(v + 1);
*runs_clone.borrow_mut() += 1;
});
assert_eq!(*runs.borrow(), 1);
}
fn observed_tracker() -> Option<super::runtime::NodeId> {
with_runtime(|rt| rt.current_tracker)
}
#[test]
fn untrack_clears_tracker_during_f_and_restores_after() {
fresh();
let observed_inside = Rc::new(RefCell::new(Some(observed_tracker()))); let observed_outer_after = Rc::new(RefCell::new(None));
let observed_inside_clone = observed_inside.clone();
let observed_outer_after_clone = observed_outer_after.clone();
effect(move || {
let outer_tracker = observed_tracker();
assert!(
outer_tracker.is_some(),
"outer effect's tracker must be set"
);
untrack(|| {
*observed_inside_clone.borrow_mut() = Some(observed_tracker());
});
*observed_outer_after_clone.borrow_mut() = Some(observed_tracker());
assert_eq!(observed_tracker(), outer_tracker);
});
assert_eq!(*observed_inside.borrow(), Some(None));
assert!(observed_outer_after.borrow().unwrap().is_some());
}
#[test]
fn untrack_is_nestable_without_breaking_outer_restore() {
fresh();
let inner_observed = Rc::new(RefCell::new(None));
let inner_observed_clone = inner_observed.clone();
effect(move || {
let outer_tracker = observed_tracker();
untrack(|| {
assert_eq!(observed_tracker(), None, "outer untrack clears");
untrack(|| {
*inner_observed_clone.borrow_mut() = Some(observed_tracker());
});
assert_eq!(observed_tracker(), None, "inner untrack returned to None");
});
assert_eq!(
observed_tracker(),
outer_tracker,
"outer untrack restored the effect's tracker"
);
});
assert_eq!(*inner_observed.borrow(), Some(None));
}
#[test]
fn untrack_restores_tracker_when_f_panics() {
use std::panic::{catch_unwind, AssertUnwindSafe};
fresh();
let restored = Rc::new(RefCell::new(None));
let restored_clone = restored.clone();
effect(move || {
let before = observed_tracker();
assert!(before.is_some(), "outer effect's tracker is set");
let result = catch_unwind(AssertUnwindSafe(|| {
untrack(|| {
panic!("intentional panic from inside untrack");
});
}));
assert!(result.is_err(), "panic must propagate");
*restored_clone.borrow_mut() = Some(observed_tracker());
assert_eq!(observed_tracker(), before, "tracker restored after panic");
});
assert!(restored.borrow().unwrap().is_some());
}
#[test]
fn computed_seed_runs_with_no_tracker_when_constructed_inside_effect() {
fresh();
let observed: Rc<RefCell<Option<Option<super::runtime::NodeId>>>> = Rc::new(RefCell::new(None));
let observed_clone = observed.clone();
effect(move || {
let observed_inner = observed_clone.clone();
let _doubled = computed(move || {
let mut slot = observed_inner.borrow_mut();
if slot.is_none() {
*slot = Some(observed_tracker());
}
0_i32
});
});
assert_eq!(
*observed.borrow(),
Some(None),
"computed seed must run with no tracker"
);
}
#[test]
fn mount_component_body_runs_with_no_tracker_when_invoked_inside_effect() {
use super::component::mount_component;
fresh();
let observed = Rc::new(RefCell::new(Some(observed_tracker())));
let observed_clone = observed.clone();
effect(move || {
let observed_inner = observed_clone.clone();
let (_owner, ()) = mount_component(0x1234_5678 as *const (), || {
*observed_inner.borrow_mut() = Some(observed_tracker());
});
});
assert_eq!(
*observed.borrow(),
Some(None),
"mount_component body must run with no tracker"
);
}
#[test]
fn mount_component_remountable_body_runs_with_no_tracker_when_invoked_inside_effect() {
use super::component::mount_component_remountable;
use crate::view::create_phantom_element;
fresh();
let observed = Rc::new(RefCell::new(Some(observed_tracker())));
let observed_clone = observed.clone();
effect(move || {
let observed_inner = observed_clone.clone();
let _root = mount_component_remountable(0x2345_6789 as *const (), move || {
*observed_inner.borrow_mut() = Some(observed_tracker());
create_phantom_element()
});
});
assert_eq!(
*observed.borrow(),
Some(None),
"mount_component_remountable body must run with no tracker"
);
}
#[test]
fn flush_mounts_callback_runs_with_no_tracker_when_called_inside_effect() {
use super::component::{flush_mounts, on_mount};
fresh();
let observed = Rc::new(RefCell::new(Some(observed_tracker())));
let observed_clone = observed.clone();
effect(move || {
let owner = super::Owner::new(None);
let observed_inner = observed_clone.clone();
owner.with(|| {
on_mount(move || {
*observed_inner.borrow_mut() = Some(observed_tracker());
});
});
flush_mounts();
});
assert_eq!(
*observed.borrow(),
Some(None),
"on_mount callback must run with no tracker"
);
}
#[test]
fn resource_sync_fetcher_runs_with_no_tracker_when_called_inside_effect() {
use super::resource::resource_sync;
fresh();
let observed = Rc::new(RefCell::new(Some(observed_tracker())));
let observed_clone = observed.clone();
effect(move || {
let observed_inner = observed_clone.clone();
let _r = resource_sync::<i32, _>(move || {
*observed_inner.borrow_mut() = Some(observed_tracker());
Ok(0)
});
});
assert_eq!(
*observed.borrow(),
Some(None),
"resource_sync fetcher must run with no tracker"
);
}
#[test]
fn computed_constructed_inside_computed_does_not_leak_subscription_to_outer_computed() {
fresh();
let (src, set_src) = signal(0_i32);
let outer_runs = Rc::new(RefCell::new(0));
let outer_runs_clone = outer_runs.clone();
let outer = computed(move || {
*outer_runs_clone.borrow_mut() += 1;
let inner = computed(move || src.get() * 2);
inner.get()
});
let initial = *outer_runs.borrow();
set_src.set(1);
flush();
let after_legit_change = *outer_runs.borrow();
assert!(
after_legit_change > initial,
"outer should re-run for legitimate edge through inner.get()"
);
assert_eq!(outer.get(), 2);
}
#[test]
fn arc_signal_basic_get_set() {
fresh();
let signal = ArcRwSignal::new(10_i32);
assert_eq!(signal.get(), 10);
signal.set(42);
assert_eq!(signal.get(), 42);
}
#[test]
fn arc_signal_clone_shares_value() {
fresh();
let s1 = ArcRwSignal::new(0_i32);
let s2 = s1.clone();
s1.set(1);
assert_eq!(s2.get(), 1);
s2.set(2);
assert_eq!(s1.get(), 2);
}
#[test]
fn arc_signal_split_round_trip() {
fresh();
let rw = ArcRwSignal::new(0_i32);
let (r, w) = rw.split();
w.set(5);
assert_eq!(r.get(), 5);
w.update(|v| *v += 3);
assert_eq!(r.get(), 8);
}
#[test]
fn arc_signal_survives_caller_owner_disposal() {
fresh();
let stash: Rc<RefCell<Option<ArcReadSignal<i32>>>> = Rc::new(RefCell::new(None));
let stash_for_install = stash.clone();
let owner = Owner::new(None);
owner.with(|| {
let (r, w) = arc_signal(99_i32);
drop(w);
*stash_for_install.borrow_mut() = Some(r);
});
owner.dispose();
let cached = stash.borrow().clone().expect("stashed signal");
assert_eq!(cached.get(), 99);
}
#[test]
fn arc_signal_effect_subscribes_and_reruns() {
fresh();
let counter = ArcRwSignal::new(0_i32);
let counter_for_effect = counter.clone();
let observed: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let observed_clone = observed.clone();
effect(move || observed_clone.borrow_mut().push(counter_for_effect.get()));
counter.set(1);
flush();
counter.set(2);
flush();
assert_eq!(*observed.borrow(), vec![0, 1, 2]);
}
#[test]
fn arc_signal_subscriber_is_pruned_when_its_owner_is_disposed() {
fresh();
let counter = ArcRwSignal::new(0_i32);
let counter_for_effect = counter.clone();
let observed: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let observed_clone = observed.clone();
let owner = Owner::new(None);
owner.with(|| {
effect(move || observed_clone.borrow_mut().push(counter_for_effect.get()));
});
assert_eq!(*observed.borrow(), vec![0]);
owner.dispose();
counter.set(7);
flush();
assert_eq!(*observed.borrow(), vec![0]);
assert_eq!(counter.get_untracked(), 7);
}
#[test]
fn arc_signal_effect_resubscribes_on_rerun() {
fresh();
let toggle = ArcRwSignal::new(false);
let a = ArcRwSignal::new(0_i32);
let b = ArcRwSignal::new(0_i32);
let toggle_e = toggle.clone();
let a_e = a.clone();
let b_e = b.clone();
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
effect(move || {
if toggle_e.get() {
let _ = a_e.get();
} else {
let _ = b_e.get();
}
*runs_clone.borrow_mut() += 1;
});
assert_eq!(*runs.borrow(), 1);
a.set(1);
flush();
assert_eq!(*runs.borrow(), 1);
b.set(1);
flush();
assert_eq!(*runs.borrow(), 2);
toggle.set(true);
flush();
assert_eq!(*runs.borrow(), 3);
b.set(2);
flush();
assert_eq!(*runs.borrow(), 3);
a.set(2);
flush();
assert_eq!(*runs.borrow(), 4);
}
#[test]
fn arc_signal_computed_caches_value() {
fresh();
let count = ArcRwSignal::new(3_i32);
let count_e = count.clone();
let doubled = computed(move || count_e.get() * 2);
assert_eq!(doubled.get(), 6);
count.set(5);
flush();
assert_eq!(doubled.get(), 10);
}
#[test]
fn arc_signal_with_untracked_does_not_register_subscriber() {
fresh();
let s = ArcRwSignal::new(0_i32);
let s_e = s.clone();
let runs = Rc::new(RefCell::new(0));
let runs_clone = runs.clone();
effect(move || {
let _ = s_e.with_untracked(|v| *v);
*runs_clone.borrow_mut() += 1;
});
assert_eq!(*runs.borrow(), 1);
s.set(1);
flush();
assert_eq!(*runs.borrow(), 1, "with_untracked must not subscribe");
}
#[test]
fn arc_to_rw_conversion_shares_underlying_value() {
fresh();
let owner = Owner::new(None);
let arc = ArcRwSignal::new(0_i32);
let arc_for_outside = arc.clone();
let rw: RwSignal<i32> = owner.with(|| arc.clone().into());
assert_eq!(rw.get(), 0);
arc.set(7);
assert_eq!(rw.get(), 7);
rw.set(42);
assert_eq!(arc.get_untracked(), 42);
assert_eq!(arc_for_outside.get_untracked(), 42);
owner.dispose();
}
#[test]
fn arc_to_rw_conversion_propagates_to_effect_subscribers() {
fresh();
let arc = ArcRwSignal::new(0_i32);
let owner = Owner::new(None);
let observed: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let observed_clone = observed.clone();
owner.with(|| {
let rw: RwSignal<i32> = arc.clone().into();
effect(move || observed_clone.borrow_mut().push(rw.get()));
});
arc.set(1);
flush();
arc.set(2);
flush();
assert_eq!(*observed.borrow(), vec![0, 1, 2]);
owner.dispose();
}
#[test]
fn arc_to_rw_conversion_survives_caller_owner_disposal_via_arc() {
fresh();
let arc = ArcRwSignal::new(0_i32);
let owner = Owner::new(None);
let _: RwSignal<i32> = owner.with(|| arc.clone().into());
owner.dispose();
arc.set(99);
assert_eq!(arc.get_untracked(), 99);
}
#[test]
fn arc_read_signal_to_read_signal_conversion_reads_correctly() {
fresh();
let (r, w) = arc_signal(5_i32);
let owner = Owner::new(None);
let copy: ReadSignal<i32> = owner.with(|| r.into());
assert_eq!(copy.get(), 5);
w.set(99);
assert_eq!(copy.get(), 99);
owner.dispose();
}
#[test]
fn arc_write_signal_to_write_signal_conversion_writes_correctly() {
fresh();
let (r, w) = arc_signal(0_i32);
let owner = Owner::new(None);
let copy: WriteSignal<i32> = owner.with(|| w.into());
copy.set(123);
assert_eq!(r.get_untracked(), 123);
owner.dispose();
}
#[test]
fn arc_signal_inside_oncelock_outlives_caller_owner() {
use std::sync::OnceLock;
fresh();
struct Module {
slot: OnceLock<ArcRwSignal<i32>>,
}
let module: Rc<Module> = Rc::new(Module {
slot: OnceLock::new(),
});
let module_clone = module.clone();
let install_owner = Owner::new(None);
install_owner.with(|| {
module_clone.slot.get_or_init(|| ArcRwSignal::new(0));
});
install_owner.dispose();
let observed: Rc<RefCell<Option<i32>>> = Rc::new(RefCell::new(None));
let observed_clone = observed.clone();
let module_for_use = module.clone();
let use_owner = Owner::new(None);
use_owner.with(|| {
effect(move || {
let v = module_for_use.slot.get().unwrap().get();
*observed_clone.borrow_mut() = Some(v);
});
});
assert_eq!(*observed.borrow(), Some(0));
module.slot.get().unwrap().set(99);
flush();
assert_eq!(*observed.borrow(), Some(99));
}
#[test]
fn paused_owner_defers_effect_runs_until_resumed() {
fresh();
let runs = Rc::new(RefCell::new(0_u32));
let (read, write) = signal(0_i32);
let owner = Owner::new(None);
let runs_clone = runs.clone();
owner.with(|| {
effect(move || {
let _ = read.get();
*runs_clone.borrow_mut() += 1;
});
});
assert_eq!(
*runs.borrow(),
1,
"initial registration runs the effect once"
);
owner.pause();
assert!(owner.is_paused());
write.set(1);
flush();
assert_eq!(*runs.borrow(), 1, "paused effect must not run on flush");
owner.resume();
assert!(!owner.is_paused());
flush();
assert_eq!(
*runs.borrow(),
2,
"resume must drain the deferred re-run into pending"
);
}
#[test]
fn pause_cascades_to_descendants() {
fresh();
let parent_runs = Rc::new(RefCell::new(0_u32));
let child_runs = Rc::new(RefCell::new(0_u32));
let (read, write) = signal(0_i32);
let parent = Owner::new(None);
let child = Owner::new(Some(parent));
let pr = parent_runs.clone();
parent.with(|| {
effect(move || {
let _ = read.get();
*pr.borrow_mut() += 1;
});
});
let cr = child_runs.clone();
child.with(|| {
effect(move || {
let _ = read.get();
*cr.borrow_mut() += 1;
});
});
assert_eq!(*parent_runs.borrow(), 1);
assert_eq!(*child_runs.borrow(), 1);
parent.pause();
assert!(parent.is_paused());
assert!(child.is_paused(), "pause cascades down the tree");
write.set(1);
flush();
assert_eq!(*parent_runs.borrow(), 1);
assert_eq!(*child_runs.borrow(), 1);
parent.resume();
flush();
assert_eq!(*parent_runs.borrow(), 2);
assert_eq!(*child_runs.borrow(), 2);
}
#[test]
fn create_owner_inherits_paused_from_parent() {
fresh();
let parent = Owner::new(None);
parent.pause();
let child = Owner::new(Some(parent));
assert!(
child.is_paused(),
"owner created under a paused parent must inherit paused"
);
}
#[test]
fn effect_registered_under_paused_owner_defers_initial_run_until_resume() {
fresh();
let runs = Rc::new(RefCell::new(0_u32));
let owner = Owner::new(None);
owner.pause();
let runs_clone = runs.clone();
owner.with(|| {
effect(move || {
*runs_clone.borrow_mut() += 1;
});
});
assert_eq!(
*runs.borrow(),
0,
"paused at registration: initial run deferred"
);
owner.resume();
flush();
assert_eq!(*runs.borrow(), 1, "resume fires the deferred initial run");
}
#[test]
fn pause_is_idempotent() {
fresh();
let owner = Owner::new(None);
owner.pause();
owner.pause();
assert!(owner.is_paused());
owner.resume();
assert!(!owner.is_paused());
owner.resume();
assert!(!owner.is_paused());
}
#[test]
fn dispose_while_paused_drops_deferred_entries() {
fresh();
let runs = Rc::new(RefCell::new(0_u32));
let (read, write) = signal(0_i32);
let owner = Owner::new(None);
let runs_clone = runs.clone();
owner.with(|| {
effect(move || {
let _ = read.get();
*runs_clone.borrow_mut() += 1;
});
});
owner.pause();
write.set(1);
flush();
assert_eq!(*runs.borrow(), 1, "paused: still 1");
owner.dispose();
flush();
assert_eq!(*runs.borrow(), 1);
}
#[test]
fn multiple_paused_signal_writes_collapse_to_one_run_on_resume() {
fresh();
let runs = Rc::new(RefCell::new(0_u32));
let (read, write) = signal(0_i32);
let owner = Owner::new(None);
let runs_clone = runs.clone();
owner.with(|| {
effect(move || {
let _ = read.get();
*runs_clone.borrow_mut() += 1;
});
});
owner.pause();
write.set(1);
flush();
write.set(2);
flush();
write.set(3);
flush();
assert_eq!(*runs.borrow(), 1, "no re-runs while paused");
owner.resume();
flush();
assert_eq!(
*runs.borrow(),
2,
"resume coalesces the deferred re-runs into a single fire"
);
}