Skip to main content

repose_core/
tests.rs

1#[cfg(test)]
2mod tests {
3    use std::cell::{Cell, RefCell};
4    use std::rc::Rc;
5
6    use crate::Color;
7    use crate::Dp;
8    use crate::Modifier;
9    use crate::Mutable;
10    use crate::Rect;
11    use crate::ScrollbarStyle;
12    use crate::Vec2;
13    use crate::animation::*;
14    use crate::error::{ErrorBoundary, throw_boundary};
15    use crate::remember_with_key;
16    use crate::runtime::ComposeGuard;
17    use crate::runtime::remember_state_auto;
18    use crate::scope::*;
19    use crate::signal::*;
20    use crate::state::StateHolder;
21    use crate::state::remember_mutable;
22    use crate::state::remember_mutable_auto;
23    use crate::state::remember_mutable_with_key;
24    use crate::state::remember_reducer_auto;
25    use crate::{View, ViewKind};
26    use crate::{
27        clear_composer, new_observer, produce_state, produce_state_eq, remove_observer,
28        run_observer_now, signal_changed,
29    };
30    use web_time::{Duration, Instant};
31
32    const FALLBACK_ID: u64 = 0xF00;
33    const CONTENT_ID: u64 = 0xC0;
34
35    fn id_view(id: u64) -> View {
36        View::new(id, ViewKind::Box)
37    }
38
39    #[test]
40    fn test_signal_basic() {
41        let sig = signal(42);
42        assert_eq!(sig.get(), 42);
43
44        sig.set(100);
45        assert_eq!(sig.get(), 100);
46
47        sig.update(|v| *v += 1);
48        assert_eq!(sig.get(), 101);
49    }
50
51    #[test]
52    fn test_signal_subscription() {
53        let sig = signal(0);
54        let called = std::rc::Rc::new(std::cell::RefCell::new(false));
55
56        let called_clone = called.clone();
57        sig.subscribe(move |_| {
58            *called_clone.borrow_mut() = true;
59        });
60
61        sig.set(42);
62        assert!(*called.borrow());
63    }
64
65    #[test]
66    fn signal_with_reads_without_clone() {
67        let sig = signal(String::from("hello"));
68        let len = sig.with(|s| s.len());
69        assert_eq!(len, 5);
70        let _ = sig.get(); // still Clone-able for get
71    }
72
73    #[test]
74    fn signal_set_neq_skips_notify() {
75        let sig = signal(0);
76        let count = Rc::new(RefCell::new(0usize));
77        let count_clone = count.clone();
78        sig.subscribe(move |_| *count_clone.borrow_mut() += 1);
79
80        sig.set_neq(0); // unchanged -> no notify
81        assert_eq!(*count.borrow(), 0);
82
83        sig.set_neq(1); // changed -> notify
84        assert_eq!(*count.borrow(), 1);
85
86        sig.set_neq(1); // unchanged again
87        assert_eq!(*count.borrow(), 1);
88    }
89
90    #[test]
91    fn test_observer_tracks_signal_read() {
92        let sig = signal(0);
93        let sig2 = sig.clone();
94        let observed = Rc::new(RefCell::new(Vec::new()));
95
96        let obs = {
97            let observed = observed.clone();
98            new_observer(move || {
99                let v = sig.get();
100                observed.borrow_mut().push(v);
101            })
102        };
103        run_observer_now(obs);
104        assert_eq!(*observed.borrow(), vec![0]);
105
106        sig2.set(42);
107        assert_eq!(*observed.borrow(), vec![0, 42]);
108
109        sig2.set(99);
110        assert_eq!(*observed.borrow(), vec![0, 42, 99]);
111
112        remove_observer(obs);
113    }
114
115    #[test]
116    fn test_observer_tracks_multiple_signals() {
117        let a = signal(1);
118        let a2 = a.clone();
119        let b = signal(2);
120        let b2 = b.clone();
121        let observed = Rc::new(RefCell::new(Vec::new()));
122
123        let obs = {
124            let observed = observed.clone();
125            new_observer(move || {
126                let sum = a.get() + b.get();
127                observed.borrow_mut().push(sum);
128            })
129        };
130        run_observer_now(obs);
131        assert_eq!(*observed.borrow(), vec![3]);
132
133        a2.set(10);
134        assert_eq!(*observed.borrow(), vec![3, 12]);
135
136        b2.set(20);
137        assert_eq!(*observed.borrow(), vec![3, 12, 30]);
138
139        remove_observer(obs);
140    }
141
142    #[test]
143    fn test_remove_observer_stops_notifications() {
144        let sig = signal(0);
145        let sig2 = sig.clone();
146        let count = Rc::new(RefCell::new(0));
147
148        let obs = {
149            let count = count.clone();
150            new_observer(move || {
151                sig.get();
152                *count.borrow_mut() += 1;
153            })
154        };
155        run_observer_now(obs);
156        assert_eq!(*count.borrow(), 1);
157
158        sig2.set(1);
159        assert_eq!(*count.borrow(), 2);
160
161        remove_observer(obs);
162
163        sig2.set(2);
164        assert_eq!(*count.borrow(), 2);
165    }
166
167    #[test]
168    fn test_reentrant_signal_write_no_panic() {
169        let a = signal(0);
170        let a2 = a.clone();
171        let b = signal(0);
172        let b2 = b.clone();
173        let observed = Rc::new(RefCell::new(Vec::new()));
174
175        let obs = {
176            let a = a.clone();
177            let observed = observed.clone();
178            new_observer(move || {
179                let bv = b.get();
180                a.set(bv);
181                observed.borrow_mut().push(bv);
182            })
183        };
184        run_observer_now(obs);
185        assert_eq!(*observed.borrow(), vec![0]);
186        assert_eq!(a2.get(), 0);
187
188        b2.set(42);
189        assert_eq!(*observed.borrow(), vec![0, 42]);
190        assert_eq!(a2.get(), 42);
191
192        a2.set(100);
193        assert_eq!(*observed.borrow(), vec![0, 42]);
194
195        remove_observer(obs);
196    }
197
198    #[test]
199    fn test_signal_changed_directly() {
200        let sig = signal(10);
201        let sig_id = sig.id();
202        let count = Rc::new(RefCell::new(0));
203
204        let obs = {
205            let count = count.clone();
206            new_observer(move || {
207                sig.get();
208                *count.borrow_mut() += 1;
209            })
210        };
211        run_observer_now(obs);
212        assert_eq!(*count.borrow(), 1);
213
214        signal_changed(sig_id);
215        assert_eq!(*count.borrow(), 2);
216
217        remove_observer(obs);
218    }
219
220    #[test]
221    fn test_observer_dead_observer_after_remove() {
222        let sig = signal(0);
223        let sig2 = sig.clone();
224        let obs = new_observer(move || {
225            sig.get();
226        });
227        run_observer_now(obs);
228        remove_observer(obs);
229
230        sig2.set(1);
231    }
232
233    #[test]
234    fn test_batch_coalesces_multi_signal_updates() {
235        use crate::batch;
236        let a = signal(1);
237        let b = signal(2);
238        let observed = Rc::new(RefCell::new(Vec::new()));
239
240        let obs = {
241            let a = a.clone();
242            let b = b.clone();
243            let observed = observed.clone();
244            new_observer(move || {
245                observed.borrow_mut().push(a.get() + b.get());
246            })
247        };
248        run_observer_now(obs);
249        assert_eq!(*observed.borrow(), vec![3]);
250
251        let (a2, b2) = (a.clone(), b.clone());
252        batch(move || {
253            a2.set(10);
254            b2.set(20);
255        });
256        assert_eq!(*observed.borrow(), vec![3, 30]);
257
258        remove_observer(obs);
259    }
260
261    #[test]
262    fn test_without_observer_reentrant_no_panic() {
263        use crate::without_observer;
264        let sig = signal(0);
265        let obs = {
266            let sig = sig.clone();
267            new_observer(move || {
268                let _ = sig.with(|v| without_observer(|| *v));
269            })
270        };
271        run_observer_now(obs);
272        sig.set(1);
273        remove_observer(obs);
274    }
275
276    #[test]
277    fn test_produce_state_tracks_dependencies() {
278        let a = signal(1);
279        let b = signal(2);
280
281        let sum = produce_state("test_sum", {
282            let a = a.clone();
283            let b = b.clone();
284            move || a.get() + b.get()
285        });
286
287        // Initial computed value
288        assert_eq!(sum.get(), 3);
289
290        a.set(10);
291        assert_eq!(sum.get(), 12);
292
293        b.set(20);
294        assert_eq!(sum.get(), 30);
295    }
296
297    #[test]
298    fn test_produce_state_chained() {
299        // Chains: a -> b -> c, where b is produce_state from a, c from b
300        let a = signal(1);
301
302        let b = produce_state("chain_b", {
303            let a = a.clone();
304            move || a.get() * 2
305        });
306        assert_eq!(b.get(), 2);
307
308        let c = produce_state("chain_c", {
309            let b = b.clone();
310            move || b.get() + 10
311        });
312        assert_eq!(c.get(), 12);
313
314        a.set(5);
315        assert_eq!(b.get(), 10);
316        assert_eq!(c.get(), 20);
317    }
318
319    #[test]
320    fn test_produce_state_eq_skips_writes() {
321        let a = signal(1);
322
323        // Subscriber count proves no write happens when the derived value
324        // is unchanged after a dependency change.
325        let writes = Rc::new(RefCell::new(0usize));
326        let eq = produce_state_eq("eq_skip", {
327            let a = a.clone();
328            move || a.get().min(10)
329        });
330        let writes_clone = writes.clone();
331        eq.subscribe(move |_| *writes_clone.borrow_mut() += 1);
332
333        assert_eq!(eq.get(), 1);
334
335        a.set(5); // min still 5 -> changed
336        assert_eq!(eq.get(), 5);
337        assert_eq!(*writes.borrow(), 1);
338
339        a.set(8); // min still 8 -> changed
340        assert_eq!(eq.get(), 8);
341        assert_eq!(*writes.borrow(), 2);
342
343        a.set(2); // min is 2, but current value 8 -> changed
344        assert_eq!(eq.get(), 2);
345        assert_eq!(*writes.borrow(), 3);
346
347        a.set(99); // min clamped to 10, current 2 -> changed
348        assert_eq!(eq.get(), 10);
349        assert_eq!(*writes.borrow(), 4);
350
351        a.set(120); // min still 10, no write
352        assert_eq!(eq.get(), 10);
353        assert_eq!(*writes.borrow(), 4);
354    }
355
356    #[test]
357    fn test_scope_cleanup_on_drop() {
358        let cleaned_up = std::rc::Rc::new(std::cell::RefCell::new(false));
359
360        {
361            let scope = Scope::new();
362            let cleaned_up_clone = cleaned_up.clone();
363            scope.add_disposer(move || {
364                *cleaned_up_clone.borrow_mut() = true;
365            });
366
367            assert!(!*cleaned_up.borrow());
368        } // ScopeInner::drop calls disposers
369
370        assert!(*cleaned_up.borrow());
371    }
372
373    #[test]
374    fn test_scope_explicit_dispose() {
375        let cleaned_up = std::rc::Rc::new(std::cell::RefCell::new(false));
376
377        let scope = Scope::new();
378        let cleaned_up_clone = cleaned_up.clone();
379        scope.add_disposer(move || {
380            *cleaned_up_clone.borrow_mut() = true;
381        });
382
383        assert!(!*cleaned_up.borrow());
384        scope.dispose();
385        assert!(*cleaned_up.borrow());
386    }
387
388    #[test]
389    fn test_key_based_remember() {
390        clear_composer();
391
392        let val1 = remember_with_key("test", || 42);
393        let val2 = remember_with_key("test", || 100);
394
395        // Should return the same instance
396        assert_eq!(*val1, 42);
397        assert_eq!(*val2, 42); // Not 100, because key exists
398    }
399
400    #[test]
401    fn test_color_from_hex() {
402        let c = Color::from_hex("#FF5733");
403        assert_eq!(c, Color(255, 87, 51, 255));
404
405        let c_alpha = Color::from_hex("#FF5733AA");
406        assert_eq!(c_alpha, Color(255, 87, 51, 170));
407    }
408
409    #[test]
410    fn test_rect_contains() {
411        let rect = Rect {
412            x: 10.0,
413            y: 10.0,
414            w: 100.0,
415            h: 50.0,
416        };
417
418        assert!(rect.contains(Vec2 { x: 50.0, y: 30.0 }));
419        assert!(!rect.contains(Vec2 { x: 5.0, y: 30.0 }));
420        assert!(!rect.contains(Vec2 { x: 50.0, y: 70.0 }));
421    }
422
423    #[test]
424    fn test_animation_deterministic() {
425        let t0 = Instant::now();
426        set_clock(Box::new(TestClock { t: t0 }));
427
428        let mut a = AnimatedValue::new(
429            0.0f32,
430            AnimationSpec::tween(Duration::from_millis(1000), Easing::Linear),
431        );
432        a.set_target(10.0);
433        // advance 250ms
434        set_clock(Box::new(TestClock {
435            t: t0 + Duration::from_millis(250),
436        }));
437        assert!(a.update());
438        assert!((*a.get() - 2.5).abs() < 0.01);
439
440        set_clock(Box::new(TestClock {
441            t: t0 + Duration::from_millis(1000),
442        }));
443        let cont = a.update();
444        assert!(!cont);
445        assert!((*a.get() - 10.0).abs() < 0.001);
446    }
447
448    #[test]
449    fn spring_target_change_preserves_velocity() {
450        let t0 = Instant::now();
451        set_clock(Box::new(TestClock { t: t0 }));
452
453        let mut animation =
454            AnimatedValue::new(100.0, AnimationSpec::spring(SpringSpec::new(0.9, 700.0)));
455        animation.set_target_with_velocity(0.0, 1000.0);
456        assert!((animation.current_velocity() - 1000.0).abs() < 0.001);
457
458        set_clock(Box::new(TestClock {
459            t: t0 + Duration::from_millis(1),
460        }));
461        assert!(animation.update());
462        assert!(*animation.get() > 100.0);
463    }
464
465    #[test]
466    fn mutable_requests_frame() {
467        clear_composer();
468        let m = remember_mutable(|| 0);
469        crate::take_frame_request(); // clear any pending request
470        assert_eq!(*m.get(), 0);
471        assert!(!crate::take_frame_request());
472
473        m.set(1);
474        assert!(crate::take_frame_request(), "set must request a frame");
475        assert_eq!(*m.get(), 1);
476
477        m.update(|v| *v += 1);
478        assert!(crate::take_frame_request(), "update must request a frame");
479        assert_eq!(*m.get(), 2);
480    }
481
482    #[test]
483    fn mutable_set_neq_skips_frame_when_unchanged() {
484        clear_composer();
485        let m = remember_mutable(|| 5);
486        crate::take_frame_request(); // clear any pending request
487
488        m.set_neq(5);
489        assert!(
490            !crate::take_frame_request(),
491            "set_neq with equal value must not request a frame"
492        );
493        assert_eq!(*m.get(), 5);
494
495        m.set_neq(7);
496        assert!(
497            crate::take_frame_request(),
498            "set_neq with different value must request a frame"
499        );
500        assert_eq!(*m.get(), 7);
501    }
502
503    #[test]
504    fn mutable_update_neq_skips_frame_when_unchanged() {
505        clear_composer();
506        let m = remember_mutable(|| 10);
507        crate::take_frame_request(); // clear any pending request
508
509        m.update_neq(|v| *v = 10);
510        assert!(
511            !crate::take_frame_request(),
512            "update_neq that leaves the value equal must not request a frame"
513        );
514
515        m.update_neq(|v| *v += 1);
516        assert!(
517            crate::take_frame_request(),
518            "update_neq that changes the value must request a frame"
519        );
520        assert_eq!(*m.get(), 11);
521    }
522
523    #[test]
524    fn mutable_with_reads_without_clone() {
525        clear_composer();
526        let m = remember_mutable(|| 3u64);
527        let doubled = m.with(|v| v * 2);
528        assert_eq!(doubled, 6);
529        assert_eq!(*m.get(), 3);
530    }
531
532    #[test]
533    fn mutable_keyed_is_stable_across_branches() {
534        clear_composer();
535        // Same key must return the same instance across calls (conditional
536        // branch stability): the second init closure must be ignored.
537        let a = remember_mutable_with_key("mv_keyed", || 42);
538        let b = remember_mutable_with_key("mv_keyed", || 0);
539        a.set(99);
540        assert_eq!(*b.get(), 99, "keyed Mutable must be stable across branches");
541    }
542
543    enum Counter {
544        Inc,
545        Reset,
546    }
547
548    struct CounterHolder;
549
550    impl StateHolder for CounterHolder {
551        type State = i32;
552        type Event = Counter;
553
554        fn initial_state() -> i32 {
555            0
556        }
557
558        fn reduce(state: &i32, event: Counter) -> i32 {
559            match event {
560                Counter::Inc => state + 1,
561                Counter::Reset => 0,
562            }
563        }
564    }
565
566    fn auto_counter() -> Mutable<i32> {
567        remember_mutable_auto("auto_counter", || 0)
568    }
569
570    fn auto_state() -> Rc<RefCell<i32>> {
571        remember_state_auto("auto_state", || 0)
572    }
573
574    fn auto_reducer() -> (Mutable<i32>, impl Fn(Counter) + Clone) {
575        remember_reducer_auto::<CounterHolder>("auto_reducer")
576    }
577
578    // Pins the identity contract the whole `*_auto` family rests on: identity is
579    // (call site, occurrence-within-frame), so N calls at one site get N distinct
580    // slots while the k-th call reuses last frame's k-th slot.
581    #[test]
582    fn auto_variants_persist_per_call_site() {
583        clear_composer();
584
585        let (_a, _b, s, _r) = {
586            let _g = ComposeGuard::begin();
587            let a = auto_counter();
588            let b = auto_counter();
589            let s = auto_state();
590            let (r, dispatch) = auto_reducer();
591            a.set(5);
592            *s.borrow_mut() = 7;
593            dispatch(Counter::Inc);
594            (a, b, s, r)
595        };
596
597        let (a2, b2, s2, r2) = {
598            let _g = ComposeGuard::begin();
599            (
600                auto_counter(),
601                auto_counter(),
602                auto_state(),
603                auto_reducer().0,
604            )
605        };
606
607        assert_eq!(
608            *a2.get(),
609            5,
610            "occurrence 0 must reuse the previous frame's slot"
611        );
612        assert_eq!(*b2.get(), 0, "occurrence 1 is a distinct slot");
613        assert!(Rc::ptr_eq(&s, &s2), "state slots must persist by call site");
614        assert_eq!(
615            *r2.get(),
616            1,
617            "reducer state must persist and keep its value"
618        );
619    }
620
621    fn build_boundary(
622        boom: Rc<RefCell<bool>>,
623        content_runs: Rc<RefCell<usize>>,
624        fallback_calls: Rc<RefCell<usize>>,
625        last_reset: Rc<RefCell<Option<Rc<dyn Fn()>>>>,
626        use_throw: bool,
627    ) -> View {
628        ErrorBoundary(
629            {
630                let fallback_calls = fallback_calls.clone();
631                move |_info, reset| {
632                    *fallback_calls.borrow_mut() += 1;
633                    *last_reset.borrow_mut() = Some(reset);
634                    id_view(FALLBACK_ID)
635                }
636            },
637            {
638                let content_runs = content_runs.clone();
639                move || {
640                    *content_runs.borrow_mut() += 1;
641                    if *boom.borrow() {
642                        if use_throw {
643                            throw_boundary("wasm-safe trip");
644                        } else {
645                            panic!("boom");
646                        }
647                    }
648                    id_view(CONTENT_ID)
649                }
650            },
651        )
652    }
653
654    #[test]
655    fn error_boundary_sticky_and_reset() {
656        clear_composer();
657        let boom = Rc::new(RefCell::new(true));
658        let content_runs = Rc::new(RefCell::new(0usize));
659        let fallback_calls = Rc::new(RefCell::new(0usize));
660        let last_reset = Rc::new(RefCell::new(None::<Rc<dyn Fn()>>));
661
662        // Frame 1: content panics -> fallback.
663        {
664            let _g = ComposeGuard::begin();
665            let v = build_boundary(
666                boom.clone(),
667                content_runs.clone(),
668                fallback_calls.clone(),
669                last_reset.clone(),
670                false,
671            );
672            assert_eq!(v.id, FALLBACK_ID);
673            assert_eq!(*fallback_calls.borrow(), 1);
674            assert_eq!(*content_runs.borrow(), 1);
675        }
676
677        // Frame 2: sticky fallback, content is NOT re-run (no repeat panic).
678        {
679            let _g = ComposeGuard::begin();
680            let v = build_boundary(
681                boom.clone(),
682                content_runs.clone(),
683                fallback_calls.clone(),
684                last_reset.clone(),
685                false,
686            );
687            assert_eq!(v.id, FALLBACK_ID);
688            assert_eq!(*fallback_calls.borrow(), 2);
689            assert_eq!(*content_runs.borrow(), 1);
690        }
691
692        // Recover and fire reset: content is re-entered.
693        *boom.borrow_mut() = false;
694        last_reset.borrow().as_ref().expect("reset captured")();
695
696        {
697            let _g = ComposeGuard::begin();
698            let v = build_boundary(
699                boom.clone(),
700                content_runs.clone(),
701                fallback_calls.clone(),
702                last_reset.clone(),
703                false,
704            );
705            assert_eq!(v.id, CONTENT_ID);
706            assert_eq!(*content_runs.borrow(), 2);
707        }
708    }
709
710    #[test]
711    fn error_boundary_throw_boundary_wasm_safe() {
712        clear_composer();
713        let boom = Rc::new(RefCell::new(true));
714        let content_runs = Rc::new(RefCell::new(0usize));
715        let fallback_calls = Rc::new(RefCell::new(0usize));
716        let last_reset = Rc::new(RefCell::new(None::<Rc<dyn Fn()>>));
717
718        {
719            let _g = ComposeGuard::begin();
720            let v = build_boundary(
721                boom.clone(),
722                content_runs.clone(),
723                fallback_calls.clone(),
724                last_reset.clone(),
725                true,
726            );
727            assert_eq!(v.id, FALLBACK_ID);
728            assert_eq!(*content_runs.borrow(), 1);
729        }
730
731        // Reset path works for throw_boundary too.
732        *boom.borrow_mut() = false;
733        last_reset.borrow().as_ref().expect("reset captured")();
734
735        {
736            let _g = ComposeGuard::begin();
737            let v = build_boundary(
738                boom.clone(),
739                content_runs.clone(),
740                fallback_calls.clone(),
741                last_reset.clone(),
742                true,
743            );
744            assert_eq!(v.id, CONTENT_ID);
745            assert_eq!(*content_runs.borrow(), 2);
746        }
747    }
748
749    #[test]
750    fn nested_scope_macro_compiles_and_bubbles_deps() {
751        use crate::Modifier;
752        crate::runtime::clear_composer();
753        let sig = signal(0);
754        let mut sched = crate::runtime::Scheduler::new();
755        let _v = crate::scope!("outer_nest_reg", &mut sched, [], {
756            let n = sig.get();
757            let _inner = crate::scope!("inner_nest_reg", &mut sched, [], {
758                let _m = sig.get();
759                View::new(0, ViewKind::Box)
760            });
761            let _ = (n, _inner);
762            View::new(0, ViewKind::Box)
763        });
764        let _ = Modifier::new();
765        sig.set(1);
766        assert!(
767            crate::scope_cache::should_run("outer_nest_reg", 0),
768            "outer scope must be dirtied by nested signal read"
769        );
770        assert!(crate::scope_cache::should_run("inner_nest_reg", 0));
771    }
772
773    #[test]
774    fn scope_keyed_accepts_owned_identifiers() {
775        clear_composer();
776        let mut scheduler = crate::runtime::Scheduler::new();
777        let id = String::from("owned");
778        let _guard = ComposeGuard::begin();
779        let _ = crate::scope_keyed!(&mut scheduler, id, [], { View::new(0, ViewKind::Box) });
780    }
781
782    #[test]
783    fn mutable_read_invalidates_cached_scope() {
784        clear_composer();
785        let mutable = remember_mutable(|| 0);
786        let mut scheduler = crate::runtime::Scheduler::new();
787        {
788            let _guard = ComposeGuard::begin();
789            let _ = crate::scope!("mutable_scope_cache", &mut scheduler, [], {
790                let _ = mutable.get();
791                View::new(0, ViewKind::Box)
792            });
793        }
794        assert!(!crate::scope_cache::should_run("mutable_scope_cache", 0));
795        mutable.set(1);
796        assert!(crate::scope_cache::should_run("mutable_scope_cache", 0));
797    }
798
799    #[test]
800    fn first_composition_signal_write_stays_dirty() {
801        clear_composer();
802        let signal = signal(0);
803        let mut scheduler = crate::runtime::Scheduler::new();
804        {
805            let _guard = ComposeGuard::begin();
806            let _ = crate::scope!("first_write_scope", &mut scheduler, [], {
807                let _ = signal.get();
808                signal.set(1);
809                View::new(0, ViewKind::Box)
810            });
811            assert!(crate::scope_cache::should_run("first_write_scope", 0));
812        }
813    }
814
815    #[test]
816    fn parent_scope_rechecks_child_inputs() {
817        clear_composer();
818        let mut scheduler = crate::runtime::Scheduler::new();
819        let child_input = RefCell::new(0);
820        {
821            let _guard = ComposeGuard::begin();
822            let _ = crate::scope!("parent_input_scope", &mut scheduler, [], {
823                let input = *child_input.borrow();
824                let _ = crate::scope!("child_input_scope", &mut scheduler, [input], {
825                    View::new(0, ViewKind::Box)
826                });
827                View::new(0, ViewKind::Box)
828            });
829        }
830        *child_input.borrow_mut() = 1;
831        assert!(crate::scope_cache::should_run("parent_input_scope", 0));
832    }
833
834    #[test]
835    fn keyed_observer_slot_is_collected() {
836        clear_composer();
837        let source = signal(1);
838        let key = "observer_gc_test";
839        {
840            let _guard = ComposeGuard::begin();
841            crate::scope_cache::with_scope_key("observer_gc_scope", || {
842                let source = source.clone();
843                let _ = produce_state(key, move || source.get());
844            });
845        }
846        assert!(!crate::runtime::COMPOSER.with(|composer| {
847            composer
848                .borrow()
849                .keyed_slots
850                .contains_key("produce:observer_gc_test")
851        }));
852        source.set(2);
853    }
854
855    #[test]
856    fn locals_track_only_values_read_by_scope() {
857        clear_composer();
858        crate::locals::set_theme_default(crate::locals::Theme::dark());
859        let mut scheduler = crate::runtime::Scheduler::new();
860        {
861            let _guard = ComposeGuard::begin();
862            let _ = crate::scope!("local_scope_cache", &mut scheduler, [], {
863                let _ = crate::locals::theme();
864                View::new(0, ViewKind::Box)
865            });
866        }
867        crate::locals::set_ui_scale_default(crate::locals::UiScale(2.0));
868        assert!(!crate::scope_cache::should_run("local_scope_cache", 0));
869        crate::locals::set_theme_default(crate::locals::Theme::light());
870        assert!(crate::scope_cache::should_run("local_scope_cache", 0));
871        crate::locals::set_theme_default(crate::locals::Theme::default());
872        crate::locals::set_ui_scale_default(crate::locals::UiScale(1.0));
873    }
874
875    #[test]
876    fn live_animation_invalidates_cached_scope() {
877        clear_composer();
878        let key = "animation_scope_cache_test";
879        let calls = Rc::new(Cell::new(0));
880        let callback_calls = calls.clone();
881        let mut scheduler = crate::runtime::Scheduler::new();
882        {
883            let _guard = ComposeGuard::begin();
884            let _ = crate::scope!("animation_scope", &mut scheduler, [], {
885                crate::animation_driver::touch(key);
886                crate::animation_driver::register(
887                    key.to_string(),
888                    Rc::new(RefCell::new(move || {
889                        let next = callback_calls.get() + 1;
890                        callback_calls.set(next);
891                        next == 1
892                    })),
893                );
894                View::new(0, ViewKind::Box)
895            });
896        }
897        assert!(crate::scope_cache::should_run("animation_scope", 0));
898        assert!(crate::animation_driver::tick());
899        assert!(crate::animation_driver::is_active());
900        crate::animation_driver::touch(key);
901        assert!(!crate::animation_driver::tick());
902        assert!(!crate::animation_driver::is_active());
903        crate::animation_driver::unregister(key);
904    }
905
906    #[test]
907    fn scope_disposers_run_lifo() {
908        let order = Rc::new(RefCell::new(Vec::new()));
909        let scope = Scope::new();
910        for value in 0..3 {
911            let order = order.clone();
912            scope.add_disposer(move || order.borrow_mut().push(value));
913        }
914        scope.dispose();
915        assert_eq!(&*order.borrow(), &[2, 1, 0]);
916    }
917
918    #[test]
919    fn then_merges_scrollbar_style() {
920        let style = ScrollbarStyle::default().with_thickness(Dp(12.0));
921        let modifier = Modifier::new().then(Modifier::new().scrollbar_style(style.clone()));
922        assert_eq!(modifier.scrollbar_style, Some(style));
923    }
924}
925
926/// Regression cover for keyed-slot liveness. A keyed slot used to be pinned to
927/// its owner scope's lifetime, so a slot abandoned inside a permanently mounted
928/// scope was never freed - which is how a closed context menu kept its overlay
929/// entry (and full-screen scrim) alive forever. Liveness is now per key per
930/// frame, with an explicit carve-out for scopes served from cache.
931#[cfg(test)]
932mod keyed_slot_liveness {
933    use crate::runtime::{ComposeGuard, Scheduler, clear_composer};
934    use crate::view::{View, ViewKind};
935
936    fn has_key(key: &str) -> bool {
937        crate::runtime::COMPOSER.with(|composer| composer.borrow().keyed_slots.contains_key(key))
938    }
939
940    #[test]
941    fn slot_abandoned_by_a_rerun_scope_is_collected() {
942        clear_composer();
943        let mut scheduler = Scheduler::new();
944        let key = "liveness_abandoned";
945        for frame in 0..2u32 {
946            let _guard = ComposeGuard::begin();
947            let _ = crate::scope!("liveness_abandoned_scope", &mut scheduler, [frame], {
948                if frame == 0 {
949                    let _ = crate::remember_with_key(key, || 7u32);
950                }
951                View::new(0, ViewKind::Box)
952            });
953        }
954        assert!(
955            !has_key(key),
956            "a key a re-run scope stopped reading must be collected"
957        );
958    }
959
960    #[test]
961    fn slot_in_a_scope_served_from_cache_is_preserved() {
962        clear_composer();
963        let mut scheduler = Scheduler::new();
964        let key = "liveness_cached";
965        for _ in 0..3 {
966            let _guard = ComposeGuard::begin();
967            let _ = crate::scope!("liveness_cached_scope", &mut scheduler, [0u32], {
968                let _ = crate::remember_with_key(key, || 7u32);
969                View::new(0, ViewKind::Box)
970            });
971        }
972        assert!(
973            has_key(key),
974            "a cached scope's slots are never read but must survive"
975        );
976    }
977
978    #[test]
979    fn slot_read_every_frame_survives() {
980        clear_composer();
981        let mut scheduler = Scheduler::new();
982        let key = "liveness_steady";
983        for _ in 0..3 {
984            let _guard = ComposeGuard::begin();
985            let _ = crate::scope!("liveness_steady_scope", &mut scheduler, [0u32], {
986                let _ = crate::remember_with_key(key, || 7u32);
987                View::new(0, ViewKind::Box)
988            });
989        }
990        assert!(has_key(key), "a consistently read slot must survive");
991    }
992}