floem_reactive/
context.rs

1use std::any::{Any, TypeId};
2
3use crate::runtime::RUNTIME;
4
5/// A marker type for context operations.
6///
7/// Provides static methods for storing and retrieving context values
8/// in the reactive scope hierarchy.
9///
10/// # Example
11/// ```rust
12/// # use floem_reactive::{Context, Scope};
13/// let scope = Scope::new();
14/// scope.enter(|| {
15///     Context::provide(42i32);
16///     Context::provide(String::from("Hello"));
17///
18///     assert_eq!(Context::get::<i32>(), Some(42));
19///     assert_eq!(Context::get::<String>(), Some(String::from("Hello")));
20/// });
21/// ```
22pub struct Context;
23
24impl Context {
25    /// Store a context value in the current scope.
26    ///
27    /// The stored context value can be retrieved by the current scope and any of its
28    /// descendants using [`Context::get`]. Child scopes can provide their own values
29    /// of the same type, which will shadow the parent's value for that subtree.
30    ///
31    /// Context values are automatically cleaned up when the scope is disposed.
32    ///
33    /// # Example
34    /// ```rust
35    /// # use floem_reactive::{Context, Scope};
36    /// let scope = Scope::new();
37    /// scope.enter(|| {
38    ///     Context::provide(42i32);
39    ///     assert_eq!(Context::get::<i32>(), Some(42));
40    /// });
41    /// ```
42    pub fn provide<T>(value: T)
43    where
44        T: Clone + 'static,
45    {
46        let ty = TypeId::of::<T>();
47
48        RUNTIME.with(|runtime| {
49            let scope = *runtime.current_scope.borrow();
50            runtime
51                .scope_contexts
52                .borrow_mut()
53                .entry(scope)
54                .or_default()
55                .insert(ty, Box::new(value) as Box<dyn Any>);
56        });
57    }
58
59    /// Try to retrieve a stored context value from the current scope or its ancestors.
60    ///
61    /// Context lookup walks up the scope tree from the current scope to find the
62    /// nearest ancestor that provides a value of the requested type. This enables
63    /// nested components to override context values for their subtrees.
64    ///
65    /// # Example
66    /// ```rust
67    /// # use floem_reactive::{Context, Scope};
68    /// let parent = Scope::new();
69    /// parent.enter(|| {
70    ///     Context::provide(42i32);
71    ///
72    ///     let child = parent.create_child();
73    ///     child.enter(|| {
74    ///         // Child sees parent's context
75    ///         assert_eq!(Context::get::<i32>(), Some(42));
76    ///     });
77    /// });
78    /// ```
79    pub fn get<T>() -> Option<T>
80    where
81        T: Clone + 'static,
82    {
83        let ty = TypeId::of::<T>();
84        RUNTIME.with(|runtime| {
85            let mut scope = *runtime.current_scope.borrow();
86            let scope_contexts = runtime.scope_contexts.borrow();
87            let parents = runtime.parents.borrow();
88
89            loop {
90                if let Some(contexts) = scope_contexts.get(&scope) {
91                    if let Some(value) = contexts.get(&ty) {
92                        return value.downcast_ref::<T>().cloned();
93                    }
94                }
95                // Walk up to parent scope
96                match parents.get(&scope) {
97                    Some(&parent) => scope = parent,
98                    None => return None,
99                }
100            }
101        })
102    }
103}
104
105/// Try to retrieve a stored Context value in the reactive system.
106///
107/// Context lookup walks up the scope tree from the current scope to find the
108/// nearest ancestor that provides a value of the requested type. This enables
109/// nested components to override context values for their subtrees.
110///
111/// # Example
112/// In a parent component:
113/// ```rust
114/// # use floem_reactive::provide_context;
115/// provide_context(42);
116/// provide_context(String::from("Hello world"));
117/// ```
118///
119/// And so in a child component you can retrieve each context data by specifying the type:
120/// ```rust
121/// # use floem_reactive::use_context;
122/// let foo: Option<i32> = use_context();
123/// let bar: Option<String> = use_context();
124/// ```
125#[deprecated(
126    since = "0.2.0",
127    note = "Use Context::get instead; this will be removed in a future release"
128)]
129pub fn use_context<T>() -> Option<T>
130where
131    T: Clone + 'static,
132{
133    let ty = TypeId::of::<T>();
134    RUNTIME.with(|runtime| {
135        let mut scope = *runtime.current_scope.borrow();
136        let scope_contexts = runtime.scope_contexts.borrow();
137        let parents = runtime.parents.borrow();
138
139        loop {
140            if let Some(contexts) = scope_contexts.get(&scope) {
141                if let Some(value) = contexts.get(&ty) {
142                    return value.downcast_ref::<T>().cloned();
143                }
144            }
145            // Walk up to parent scope
146            match parents.get(&scope) {
147                Some(&parent) => scope = parent,
148                None => return None,
149            }
150        }
151    })
152}
153
154/// Sets a context value to be stored in the current scope.
155///
156/// The stored context value can be retrieved by the current scope and any of its
157/// descendants using [use_context](use_context). Child scopes can provide their
158/// own values of the same type, which will shadow the parent's value for that
159/// subtree.
160///
161/// Context values are automatically cleaned up when the scope is disposed.
162///
163/// # Example
164/// In a parent component:
165/// ```rust
166/// # use floem_reactive::provide_context;
167/// provide_context(42);
168/// provide_context(String::from("Hello world"));
169/// ```
170///
171/// And so in a child component you can retrieve each context data by specifying the type:
172/// ```rust
173/// # use floem_reactive::use_context;
174/// let foo: Option<i32> = use_context();
175/// let bar: Option<String> = use_context();
176/// ```
177#[deprecated(
178    since = "0.2.0",
179    note = "Use Context::provide instead; this will be removed in a future release"
180)]
181pub fn provide_context<T>(value: T)
182where
183    T: Clone + 'static,
184{
185    let ty = TypeId::of::<T>();
186
187    RUNTIME.with(|runtime| {
188        let scope = *runtime.current_scope.borrow();
189        runtime
190            .scope_contexts
191            .borrow_mut()
192            .entry(scope)
193            .or_default()
194            .insert(ty, Box::new(value) as Box<dyn Any>);
195    });
196}
197
198#[cfg(test)]
199mod tests {
200    use crate::scope::Scope;
201
202    use super::*;
203
204    #[test]
205    fn context_in_same_scope() {
206        let scope = Scope::new();
207        scope.enter(|| {
208            provide_context(42i32);
209            assert_eq!(use_context::<i32>(), Some(42));
210        });
211    }
212
213    #[test]
214    fn context_inherited_from_parent() {
215        let parent = Scope::new();
216        parent.enter(|| {
217            provide_context(42i32);
218
219            let child = parent.create_child();
220            child.enter(|| {
221                // Child should see parent's context
222                assert_eq!(use_context::<i32>(), Some(42));
223            });
224        });
225    }
226
227    #[test]
228    fn context_shadowing_in_child() {
229        let parent = Scope::new();
230        parent.enter(|| {
231            provide_context(42i32);
232
233            let child = parent.create_child();
234            child.enter(|| {
235                // Override in child scope
236                provide_context(100i32);
237                assert_eq!(use_context::<i32>(), Some(100));
238            });
239
240            // Parent still has original value
241            assert_eq!(use_context::<i32>(), Some(42));
242        });
243    }
244
245    #[test]
246    fn sibling_scopes_isolated() {
247        let parent = Scope::new();
248        parent.enter(|| {
249            provide_context(0i32);
250
251            let child1 = parent.create_child();
252            let child2 = parent.create_child();
253
254            child1.enter(|| {
255                provide_context(1i32);
256                assert_eq!(use_context::<i32>(), Some(1));
257            });
258
259            child2.enter(|| {
260                provide_context(2i32);
261                assert_eq!(use_context::<i32>(), Some(2));
262            });
263
264            // Verify they're still isolated
265            child1.enter(|| {
266                assert_eq!(use_context::<i32>(), Some(1));
267            });
268
269            child2.enter(|| {
270                assert_eq!(use_context::<i32>(), Some(2));
271            });
272        });
273    }
274
275    #[test]
276    fn context_cleaned_up_on_dispose() {
277        let parent = Scope::new();
278        let child_value = parent.enter(|| {
279            provide_context(42i32);
280
281            let child = parent.create_child();
282            let value = child.enter(|| {
283                provide_context(100i32);
284                use_context::<i32>()
285            });
286
287            // Dispose child
288            child.dispose();
289
290            value
291        });
292
293        assert_eq!(child_value, Some(100));
294
295        // After dispose, parent context should still work
296        parent.enter(|| {
297            assert_eq!(use_context::<i32>(), Some(42));
298        });
299    }
300
301    #[test]
302    fn deeply_nested_context_lookup() {
303        let root = Scope::new();
304        root.enter(|| {
305            provide_context(String::from("root"));
306
307            let level1 = root.create_child();
308            level1.enter(|| {
309                let level2 = level1.create_child();
310                level2.enter(|| {
311                    let level3 = level2.create_child();
312                    level3.enter(|| {
313                        // Should find root's context 3 levels up
314                        assert_eq!(use_context::<String>(), Some(String::from("root")));
315                    });
316                });
317            });
318        });
319    }
320
321    #[test]
322    fn multiple_context_types() {
323        let scope = Scope::new();
324        scope.enter(|| {
325            provide_context(42i32);
326            provide_context(String::from("hello"));
327            provide_context(3.15f64);
328
329            assert_eq!(use_context::<i32>(), Some(42));
330            assert_eq!(use_context::<String>(), Some(String::from("hello")));
331            assert_eq!(use_context::<f64>(), Some(3.15));
332            assert_eq!(use_context::<bool>(), None);
333        });
334    }
335
336    #[test]
337    fn context_not_found_returns_none() {
338        let scope = Scope::new();
339        scope.enter(|| {
340            // No context provided, should return None
341            assert_eq!(use_context::<i32>(), None);
342            assert_eq!(use_context::<String>(), None);
343        });
344    }
345
346    #[test]
347    fn overwrite_context_in_same_scope() {
348        let scope = Scope::new();
349        scope.enter(|| {
350            provide_context(42i32);
351            assert_eq!(use_context::<i32>(), Some(42));
352
353            // Overwrite in same scope
354            provide_context(100i32);
355            assert_eq!(use_context::<i32>(), Some(100));
356        });
357    }
358
359    #[test]
360    fn parent_disposal_cleans_up_children() {
361        let parent = Scope::new();
362        let child = parent.create_child();
363
364        parent.enter(|| {
365            provide_context(42i32);
366        });
367
368        child.enter(|| {
369            provide_context(100i32);
370        });
371
372        // Verify contexts exist
373        parent.enter(|| {
374            assert_eq!(use_context::<i32>(), Some(42));
375        });
376        child.enter(|| {
377            assert_eq!(use_context::<i32>(), Some(100));
378        });
379
380        // Dispose parent - should clean up child too
381        parent.dispose();
382
383        // Verify parent's context is gone (scope no longer has context)
384        RUNTIME.with(|runtime| {
385            assert!(runtime.scope_contexts.borrow().get(&parent.0).is_none());
386            assert!(runtime.scope_contexts.borrow().get(&child.0).is_none());
387            assert!(runtime.parents.borrow().get(&child.0).is_none());
388        });
389    }
390
391    #[test]
392    fn context_shadowing_at_multiple_levels() {
393        let root = Scope::new();
394        root.enter(|| {
395            provide_context(String::from("root"));
396
397            let level1 = root.create_child();
398            level1.enter(|| {
399                // Don't provide at level1, should inherit from root
400                assert_eq!(use_context::<String>(), Some(String::from("root")));
401
402                let level2 = level1.create_child();
403                level2.enter(|| {
404                    // Shadow at level2
405                    provide_context(String::from("level2"));
406                    assert_eq!(use_context::<String>(), Some(String::from("level2")));
407
408                    let level3 = level2.create_child();
409                    level3.enter(|| {
410                        // Should see level2's value, not root's
411                        assert_eq!(use_context::<String>(), Some(String::from("level2")));
412                    });
413                });
414
415                // Back at level1, should still see root's value
416                assert_eq!(use_context::<String>(), Some(String::from("root")));
417            });
418        });
419    }
420
421    #[test]
422    fn child_created_via_set_scope_inherits_context() {
423        use crate::id::Id;
424
425        let parent = Scope::new();
426        parent.enter(|| {
427            provide_context(42i32);
428
429            // Simulate what happens when a signal is created - it calls set_scope
430            let child_id = Id::next();
431            child_id.set_scope();
432
433            // The child_id should now have parent as its parent
434            RUNTIME.with(|runtime| {
435                let parents = runtime.parents.borrow();
436                assert_eq!(parents.get(&child_id), Some(&parent.0));
437            });
438        });
439    }
440
441    #[test]
442    fn orphan_scope_has_no_parent_context() {
443        // Scope::new() creates a scope with no parent
444        let orphan = Scope::new();
445        orphan.enter(|| {
446            // No parent, so no inherited context
447            assert_eq!(use_context::<i32>(), None);
448
449            // But can still provide its own context
450            provide_context(42i32);
451            assert_eq!(use_context::<i32>(), Some(42));
452        });
453    }
454
455    #[test]
456    fn newtype_wrappers_are_distinct_types() {
457        #[derive(Clone, PartialEq, Debug)]
458        struct UserId(i32);
459
460        #[derive(Clone, PartialEq, Debug)]
461        struct PostId(i32);
462
463        let scope = Scope::new();
464        scope.enter(|| {
465            provide_context(UserId(1));
466            provide_context(PostId(2));
467
468            // Same underlying type (i32) but different wrapper types
469            assert_eq!(use_context::<UserId>(), Some(UserId(1)));
470            assert_eq!(use_context::<PostId>(), Some(PostId(2)));
471
472            // Raw i32 is not provided
473            assert_eq!(use_context::<i32>(), None);
474        });
475    }
476
477    #[test]
478    fn context_with_rw_signal() {
479        use crate::{create_rw_signal, SignalGet, SignalUpdate};
480
481        let scope = Scope::new();
482        scope.enter(|| {
483            let signal = create_rw_signal(42);
484            provide_context(signal);
485
486            // Retrieve the signal from context
487            let retrieved = use_context::<crate::signal::RwSignal<i32>>().unwrap();
488            assert_eq!(retrieved.get_untracked(), 42);
489
490            // Modifying the retrieved signal should affect the original
491            // (they're the same signal, just cloned handle)
492            retrieved.set(100);
493            assert_eq!(signal.get_untracked(), 100);
494        });
495    }
496
497    #[test]
498    fn dispose_middle_of_hierarchy() {
499        let root = Scope::new();
500        let middle = root.create_child();
501        let leaf = middle.create_child();
502
503        root.enter(|| provide_context(String::from("root")));
504        middle.enter(|| provide_context(String::from("middle")));
505        leaf.enter(|| provide_context(String::from("leaf")));
506
507        // Verify all contexts exist
508        RUNTIME.with(|runtime| {
509            assert!(runtime.scope_contexts.borrow().contains_key(&root.0));
510            assert!(runtime.scope_contexts.borrow().contains_key(&middle.0));
511            assert!(runtime.scope_contexts.borrow().contains_key(&leaf.0));
512        });
513
514        // Dispose middle - should clean up leaf too
515        middle.dispose();
516
517        RUNTIME.with(|runtime| {
518            // Root should still exist
519            assert!(runtime.scope_contexts.borrow().contains_key(&root.0));
520            // Middle and leaf should be gone
521            assert!(!runtime.scope_contexts.borrow().contains_key(&middle.0));
522            assert!(!runtime.scope_contexts.borrow().contains_key(&leaf.0));
523            // Parent tracking for leaf should be gone
524            assert!(!runtime.parents.borrow().contains_key(&leaf.0));
525        });
526
527        // Root context still works
528        root.enter(|| {
529            assert_eq!(use_context::<String>(), Some(String::from("root")));
530        });
531    }
532
533    #[test]
534    fn create_child_outside_enter() {
535        let parent = Scope::new();
536
537        // Provide context while inside parent
538        parent.enter(|| {
539            provide_context(42i32);
540        });
541
542        // Create child outside of enter - should still establish parent relationship
543        let child = parent.create_child();
544
545        // Child should be able to see parent's context
546        child.enter(|| {
547            assert_eq!(use_context::<i32>(), Some(42));
548        });
549    }
550
551    #[test]
552    fn context_not_visible_to_parent() {
553        let parent = Scope::new();
554        let child = parent.create_child();
555
556        // Child provides context
557        child.enter(|| {
558            provide_context(100i32);
559        });
560
561        // Parent should NOT see child's context
562        parent.enter(|| {
563            assert_eq!(use_context::<i32>(), None);
564        });
565    }
566
567    #[test]
568    fn scope_reentry_preserves_context() {
569        let scope = Scope::new();
570
571        scope.enter(|| {
572            provide_context(42i32);
573        });
574
575        // Re-enter the same scope
576        scope.enter(|| {
577            // Context should still be there
578            assert_eq!(use_context::<i32>(), Some(42));
579
580            // Can update it
581            provide_context(100i32);
582        });
583
584        // And it persists
585        scope.enter(|| {
586            assert_eq!(use_context::<i32>(), Some(100));
587        });
588    }
589
590    #[test]
591    fn nested_enter_same_scope() {
592        let scope = Scope::new();
593
594        scope.enter(|| {
595            provide_context(42i32);
596
597            // Nested enter of the same scope
598            scope.enter(|| {
599                assert_eq!(use_context::<i32>(), Some(42));
600                provide_context(100i32);
601            });
602
603            // After nested enter returns, we're back in the first enter
604            // The context was updated
605            assert_eq!(use_context::<i32>(), Some(100));
606        });
607    }
608
609    #[test]
610    fn context_with_enter_child() {
611        let parent = Scope::new();
612
613        parent.enter(|| {
614            provide_context(42i32);
615
616            // enter_child creates a child scope and wraps a closure
617            let make_child = parent.enter_child(|multiplier: i32| {
618                // Should see parent's context
619                let val = use_context::<i32>().unwrap();
620                // Can provide own context
621                provide_context(String::from("child"));
622                val * multiplier
623            });
624
625            // Call the wrapped closure
626            let (result, child_scope) = make_child(2);
627            assert_eq!(result, 84);
628
629            // Child scope has its own context
630            child_scope.enter(|| {
631                assert_eq!(use_context::<String>(), Some(String::from("child")));
632                // But still inherits parent's i32
633                assert_eq!(use_context::<i32>(), Some(42));
634            });
635
636            // Parent doesn't see child's String context
637            assert_eq!(use_context::<String>(), None);
638        });
639    }
640
641    #[test]
642    fn context_visible_in_effect() {
643        use crate::{create_effect, create_rw_signal, SignalGet};
644        use std::cell::Cell;
645        use std::rc::Rc;
646
647        let scope = Scope::new();
648        let seen_value = Rc::new(Cell::new(0i32));
649
650        scope.enter(|| {
651            provide_context(42i32);
652            let trigger = create_rw_signal(0);
653
654            let seen = seen_value.clone();
655            create_effect(move |_| {
656                trigger.get(); // Subscribe to trigger
657                               // Effect should see the context from the scope it was created in
658                if let Some(val) = use_context::<i32>() {
659                    seen.set(val);
660                }
661            });
662        });
663
664        // Effect runs immediately on creation
665        assert_eq!(seen_value.get(), 42);
666    }
667
668    #[test]
669    fn zero_sized_type_as_context_marker() {
670        // ZST marker types are a common pattern
671        #[derive(Clone, Debug, PartialEq)]
672        struct DarkMode;
673
674        #[derive(Clone, Debug, PartialEq)]
675        struct DebugEnabled;
676
677        let scope = Scope::new();
678        scope.enter(|| {
679            // Provide marker
680            provide_context(DarkMode);
681
682            // Check presence
683            assert!(use_context::<DarkMode>().is_some());
684            assert!(use_context::<DebugEnabled>().is_none());
685
686            let child = scope.create_child();
687            child.enter(|| {
688                // Child inherits marker
689                assert!(use_context::<DarkMode>().is_some());
690
691                // Child can add its own marker
692                provide_context(DebugEnabled);
693                assert!(use_context::<DebugEnabled>().is_some());
694            });
695
696            // Parent doesn't see child's marker
697            assert!(use_context::<DebugEnabled>().is_none());
698        });
699    }
700
701    // Tests for the new Context struct API
702    #[test]
703    fn context_struct_provide_and_get() {
704        let scope = Scope::new();
705        scope.enter(|| {
706            Context::provide(42i32);
707            Context::provide(String::from("hello"));
708
709            assert_eq!(Context::get::<i32>(), Some(42));
710            assert_eq!(Context::get::<String>(), Some(String::from("hello")));
711            assert_eq!(Context::get::<f64>(), None);
712        });
713    }
714
715    #[test]
716    fn context_struct_inheritance() {
717        let parent = Scope::new();
718        parent.enter(|| {
719            Context::provide(42i32);
720
721            let child = parent.create_child();
722            child.enter(|| {
723                // Child sees parent's context
724                assert_eq!(Context::get::<i32>(), Some(42));
725
726                // Child can shadow
727                Context::provide(100i32);
728                assert_eq!(Context::get::<i32>(), Some(100));
729            });
730
731            // Parent still has original
732            assert_eq!(Context::get::<i32>(), Some(42));
733        });
734    }
735
736    // Tests for Scope::provide_context and Scope::get_context
737    #[test]
738    fn scope_provide_and_get_context() {
739        let scope = Scope::new();
740        scope.provide_context(42i32);
741        scope.provide_context(String::from("hello"));
742
743        assert_eq!(scope.get_context::<i32>(), Some(42));
744        assert_eq!(scope.get_context::<String>(), Some(String::from("hello")));
745        assert_eq!(scope.get_context::<f64>(), None);
746    }
747
748    #[test]
749    fn scope_context_inheritance() {
750        let parent = Scope::new();
751        parent.provide_context(42i32);
752
753        let child = parent.create_child();
754        // Child should see parent's context
755        assert_eq!(child.get_context::<i32>(), Some(42));
756
757        // Child can provide its own
758        child.provide_context(100i32);
759        assert_eq!(child.get_context::<i32>(), Some(100));
760
761        // Parent still has original
762        assert_eq!(parent.get_context::<i32>(), Some(42));
763    }
764
765    #[test]
766    fn scope_context_not_visible_to_parent() {
767        let parent = Scope::new();
768        let child = parent.create_child();
769
770        child.provide_context(100i32);
771
772        // Parent should NOT see child's context
773        assert_eq!(parent.get_context::<i32>(), None);
774        assert_eq!(child.get_context::<i32>(), Some(100));
775    }
776
777    #[test]
778    fn dispose_cleans_up_multiple_children() {
779        let parent = Scope::new();
780        let child1 = parent.create_child();
781        let child2 = parent.create_child();
782        let child3 = parent.create_child();
783
784        parent.provide_context(String::from("parent"));
785        child1.provide_context(String::from("child1"));
786        child2.provide_context(String::from("child2"));
787        child3.provide_context(String::from("child3"));
788
789        // Verify all contexts exist
790        RUNTIME.with(|runtime| {
791            assert!(runtime.scope_contexts.borrow().contains_key(&parent.0));
792            assert!(runtime.scope_contexts.borrow().contains_key(&child1.0));
793            assert!(runtime.scope_contexts.borrow().contains_key(&child2.0));
794            assert!(runtime.scope_contexts.borrow().contains_key(&child3.0));
795        });
796
797        // Dispose parent - should clean up all children
798        parent.dispose();
799
800        RUNTIME.with(|runtime| {
801            // All should be gone
802            assert!(!runtime.scope_contexts.borrow().contains_key(&parent.0));
803            assert!(!runtime.scope_contexts.borrow().contains_key(&child1.0));
804            assert!(!runtime.scope_contexts.borrow().contains_key(&child2.0));
805            assert!(!runtime.scope_contexts.borrow().contains_key(&child3.0));
806            // Parent tracking should be gone for all children
807            assert!(!runtime.parents.borrow().contains_key(&child1.0));
808            assert!(!runtime.parents.borrow().contains_key(&child2.0));
809            assert!(!runtime.parents.borrow().contains_key(&child3.0));
810        });
811    }
812
813    #[test]
814    fn dispose_parent_without_context_cleans_children_context() {
815        let parent = Scope::new();
816        let child1 = parent.create_child();
817        let child2 = parent.create_child();
818
819        // Parent has NO context, but children do
820        child1.provide_context(String::from("child1"));
821        child2.provide_context(String::from("child2"));
822
823        // Verify children's contexts exist
824        RUNTIME.with(|runtime| {
825            assert!(!runtime.scope_contexts.borrow().contains_key(&parent.0));
826            assert!(runtime.scope_contexts.borrow().contains_key(&child1.0));
827            assert!(runtime.scope_contexts.borrow().contains_key(&child2.0));
828        });
829
830        // Dispose parent - should still clean up children's contexts
831        parent.dispose();
832
833        RUNTIME.with(|runtime| {
834            assert!(!runtime.scope_contexts.borrow().contains_key(&child1.0));
835            assert!(!runtime.scope_contexts.borrow().contains_key(&child2.0));
836            assert!(!runtime.parents.borrow().contains_key(&child1.0));
837            assert!(!runtime.parents.borrow().contains_key(&child2.0));
838        });
839    }
840
841    #[test]
842    fn double_dispose_is_idempotent() {
843        let scope = Scope::new();
844        let child = scope.create_child();
845
846        scope.provide_context(42i32);
847        child.provide_context(100i32);
848
849        // First dispose
850        scope.dispose();
851
852        RUNTIME.with(|runtime| {
853            assert!(!runtime.scope_contexts.borrow().contains_key(&scope.0));
854            assert!(!runtime.scope_contexts.borrow().contains_key(&child.0));
855        });
856
857        // Second dispose - should not panic
858        scope.dispose();
859        child.dispose();
860
861        // Still clean
862        RUNTIME.with(|runtime| {
863            assert!(!runtime.scope_contexts.borrow().contains_key(&scope.0));
864            assert!(!runtime.scope_contexts.borrow().contains_key(&child.0));
865        });
866    }
867}