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(); }
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); assert_eq!(*count.borrow(), 0);
82
83 sig.set_neq(1); assert_eq!(*count.borrow(), 1);
85
86 sig.set_neq(1); 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 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 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 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); assert_eq!(eq.get(), 5);
337 assert_eq!(*writes.borrow(), 1);
338
339 a.set(8); assert_eq!(eq.get(), 8);
341 assert_eq!(*writes.borrow(), 2);
342
343 a.set(2); assert_eq!(eq.get(), 2);
345 assert_eq!(*writes.borrow(), 3);
346
347 a.set(99); assert_eq!(eq.get(), 10);
349 assert_eq!(*writes.borrow(), 4);
350
351 a.set(120); 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 } 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 assert_eq!(*val1, 42);
397 assert_eq!(*val2, 42); }
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 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(); 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(); 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(); 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 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 #[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 {
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 {
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 *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 *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#[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}