Skip to main content

pax_runtime/
router.rs

1//! Runtime routing primitives used by declarative `Router` / `Route` control flow.
2
3use std::collections::HashMap;
4use std::iter;
5use std::ops::Range;
6use std::rc::Rc;
7use_RefCell!();
8
9use pax_manifest::ControlFlowRouteBranchDefinition;
10use pax_runtime_api::pax_value::ImplToFromPaxAny;
11use pax_runtime_api::{
12    borrow, borrow_mut, use_RefCell, Interpolatable, LocalProperty, PaxValue, ToPaxValue, Variable,
13};
14
15use crate::api::Layer;
16use crate::{
17    BaseInstance, ExpandedNode, InstanceFlags, InstanceNode, InstantiationArgs, RuntimeContext,
18    RuntimePropertiesStackFrame,
19};
20
21/// Internal stack symbol carrying the router input location for nested scopes.
22pub const INTERNAL_ROUTE_LOCATION_SYMBOL: &str = "$route_location";
23/// Internal stack symbol carrying the current route match object.
24pub const INTERNAL_ROUTE_MATCH_SYMBOL: &str = "$route_match";
25const ROUTE_SYMBOL: &str = "route";
26
27/// Structured application location shared across platforms.
28///
29/// On web targets this is serialized to and from `window.location`, while on
30/// non-web targets it remains a platform-agnostic route state model.
31#[derive(Clone, Debug, Default, PartialEq, Eq)]
32pub struct RouteLocation {
33    /// Decoded path segments, with `/` represented by an empty vector.
34    pub path_segments: Vec<String>,
35    /// Query-string values keyed by parameter name, preserving repeated keys.
36    pub query: HashMap<String, Vec<String>>,
37    /// Optional fragment without the leading `#`.
38    pub fragment: Option<String>,
39}
40
41impl RouteLocation {
42    /// Returns the canonical root location.
43    pub fn root() -> Self {
44        Self::default()
45    }
46
47    /// Clones this location while replacing only the path segments.
48    pub fn with_path_segments(&self, path_segments: Vec<String>) -> Self {
49        Self {
50            path_segments,
51            query: self.query.clone(),
52            fragment: self.fragment.clone(),
53        }
54    }
55}
56
57impl From<pax_message::RouteChangeInterruptArgs> for RouteLocation {
58    fn from(value: pax_message::RouteChangeInterruptArgs) -> Self {
59        Self {
60            path_segments: value.path_segments,
61            query: value.query,
62            fragment: value.fragment,
63        }
64    }
65}
66
67impl From<&pax_message::RouteChangeInterruptArgs> for RouteLocation {
68    fn from(value: &pax_message::RouteChangeInterruptArgs) -> Self {
69        Self {
70            path_segments: value.path_segments.clone(),
71            query: value.query.clone(),
72            fragment: value.fragment.clone(),
73        }
74    }
75}
76
77impl Interpolatable for RouteLocation {}
78
79impl ToPaxValue for RouteLocation {
80    fn to_pax_value(self) -> PaxValue {
81        PaxValue::Object(vec![
82            (
83                "path_segments".to_string(),
84                self.path_segments.to_pax_value(),
85            ),
86            ("query".to_string(), multimap_to_pax_value(&self.query)),
87            ("fragment".to_string(), self.fragment.to_pax_value()),
88        ])
89    }
90}
91
92/// Structured match data exposed to the active route subtree as `route`.
93#[derive(Clone, Debug, Default, PartialEq, Eq)]
94pub struct RouteMatch {
95    /// Location as seen by the current router scope.
96    pub location: RouteLocation,
97    /// Full global application location.
98    pub global_location: RouteLocation,
99    /// Named values captured from `:param` segments.
100    pub params: HashMap<String, String>,
101    /// Number of path segments consumed by the selected branch.
102    pub consumed_segments: usize,
103    /// Remaining path tail left after the selected branch.
104    pub remainder: Vec<String>,
105    /// Whether the selected branch consumed the entire scoped path.
106    pub is_exact: bool,
107}
108
109impl RouteMatch {
110    /// Converts the remainder into a `RouteLocation` for nested router input.
111    pub fn remainder_location(&self) -> RouteLocation {
112        self.location.with_path_segments(self.remainder.clone())
113    }
114
115    fn default_match(location: &RouteLocation, global_location: &RouteLocation) -> Self {
116        Self {
117            location: location.clone(),
118            global_location: global_location.clone(),
119            params: HashMap::new(),
120            consumed_segments: 0,
121            remainder: location.path_segments.clone(),
122            is_exact: location.path_segments.is_empty(),
123        }
124    }
125}
126
127impl Interpolatable for RouteMatch {}
128
129impl ToPaxValue for RouteMatch {
130    fn to_pax_value(self) -> PaxValue {
131        PaxValue::Object(vec![
132            ("location".to_string(), self.location.to_pax_value()),
133            (
134                "global_location".to_string(),
135                self.global_location.to_pax_value(),
136            ),
137            ("params".to_string(), string_map_to_pax_value(&self.params)),
138            (
139                "consumed_segments".to_string(),
140                self.consumed_segments.to_pax_value(),
141            ),
142            ("remainder".to_string(), self.remainder.to_pax_value()),
143            ("is_exact".to_string(), self.is_exact.to_pax_value()),
144        ])
145    }
146}
147
148/// Internal router inputs carried into a `RouterInstance`.
149#[derive(Default)]
150pub struct RouterProperties {
151    /// Location scoped to the current router.
152    pub input_location: LocalProperty<RouteLocation>,
153    /// Full application location retained for diagnostics and coordination.
154    pub global_location: LocalProperty<RouteLocation>,
155}
156
157impl ImplToFromPaxAny for RouterProperties {}
158
159impl ToPaxValue for RouterProperties {
160    fn to_pax_value(self) -> PaxValue {
161        PaxValue::Object(vec![
162            (
163                "input_location".to_string(),
164                self.input_location.to_pax_value(),
165            ),
166            (
167                "global_location".to_string(),
168                self.global_location.to_pax_value(),
169            ),
170        ])
171    }
172}
173
174#[derive(Clone, Debug, PartialEq, Eq)]
175enum RouteSegmentPattern {
176    Literal(String),
177    Param(String),
178    CatchAll,
179}
180
181/// Compiled route branch used by the runtime matcher.
182#[derive(Clone, Debug)]
183pub struct CompiledRouteBranch {
184    /// Whether this branch is the fallback `default=true` branch.
185    pub default: bool,
186    /// Whether this branch should stack over the previously active branch.
187    pub modal: bool,
188    pattern: Vec<RouteSegmentPattern>,
189}
190
191impl CompiledRouteBranch {
192    fn from_definition(definition: &ControlFlowRouteBranchDefinition) -> Self {
193        Self {
194            default: definition.default,
195            modal: definition.modal,
196            pattern: definition
197                .path
198                .as_deref()
199                .map(compile_route_pattern)
200                .unwrap_or_default(),
201        }
202    }
203}
204
205#[derive(Clone, Debug, Default, PartialEq, Eq)]
206struct RouteSelection {
207    branch_index: Option<usize>,
208    route_match: Option<RouteMatch>,
209}
210
211pub struct RouterInstance {
212    base: BaseInstance,
213    branches: Vec<CompiledRouteBranch>,
214    branch_child_ranges: Vec<Range<usize>>,
215}
216
217impl InstanceNode for RouterInstance {
218    fn instantiate(args: InstantiationArgs) -> Rc<Self>
219    where
220        Self: Sized,
221    {
222        Rc::new(Self {
223            base: BaseInstance::new(
224                args,
225                InstanceFlags {
226                    invisible_to_slot: true,
227                    invisible_to_raycasting: true,
228                    layer: Layer::DontCare,
229                    is_component: false,
230                    is_slot: false,
231                },
232            ),
233            branches: vec![],
234            branch_child_ranges: vec![],
235        })
236    }
237
238    fn handle_mount(
239        self: Rc<Self>,
240        expanded_node: &Rc<ExpandedNode>,
241        context: &Rc<RuntimeContext>,
242    ) {
243        self.handle_setup(expanded_node, context, true);
244    }
245
246    fn handle_control_flow_node_expansion(
247        self: Rc<Self>,
248        expanded_node: &Rc<ExpandedNode>,
249        context: &Rc<RuntimeContext>,
250    ) {
251        self.handle_setup(expanded_node, context, false);
252    }
253
254    fn resolve_debug(
255        &self,
256        f: &mut std::fmt::Formatter,
257        _expanded_node: Option<&ExpandedNode>,
258    ) -> std::fmt::Result {
259        f.debug_struct("Router").finish()
260    }
261
262    fn base(&self) -> &BaseInstance {
263        &self.base
264    }
265}
266
267impl RouterInstance {
268    /// Creates a router instance with precompiled route branches.
269    pub fn instantiate_with_branches(
270        args: InstantiationArgs,
271        branches: Vec<CompiledRouteBranch>,
272        branch_child_ranges: Vec<Range<usize>>,
273    ) -> Rc<Self> {
274        Rc::new(Self {
275            base: BaseInstance::new(
276                args,
277                InstanceFlags {
278                    invisible_to_slot: true,
279                    invisible_to_raycasting: true,
280                    layer: Layer::DontCare,
281                    is_component: false,
282                    is_slot: false,
283                },
284            ),
285            branches,
286            branch_child_ranges,
287        })
288    }
289
290    fn handle_setup(
291        self: Rc<Self>,
292        expanded_node: &Rc<ExpandedNode>,
293        context: &Rc<RuntimeContext>,
294        is_mount: bool,
295    ) {
296        let weak_ref_self = Rc::downgrade(expanded_node);
297        let cloned_self = Rc::clone(&self);
298        let cloned_context = Rc::clone(context);
299
300        let input_location =
301            expanded_node.with_properties_unwrapped(|properties: &mut RouterProperties| {
302                properties.input_location.clone()
303            });
304        let global_location =
305            expanded_node.with_properties_unwrapped(|properties: &mut RouterProperties| {
306                properties.global_location.clone()
307            });
308
309        let deps = vec![input_location.untyped(), global_location.untyped()];
310        let branches = self.branches.clone();
311        let branch_child_ranges = self.branch_child_ranges.clone();
312        let old_selection = RefCell::new(RouteSelection::default());
313
314        expanded_node
315            .children
316            .replace_with(LocalProperty::computed_with_name(
317                move || {
318                    let Some(cloned_expanded_node) = weak_ref_self.upgrade() else {
319                        panic!("ran evaluator after expanded node dropped (router)")
320                    };
321
322                    let input = input_location.get();
323                    let global = global_location.get();
324                    let selection = select_route_branch(&branches, &input, &global);
325
326                    if *borrow!(old_selection) == selection {
327                        return cloned_expanded_node.current_attached_children();
328                    }
329                    *borrow_mut!(old_selection) = selection.clone();
330
331                    let selected_branch = selection.branch_index.and_then(|branch_index| {
332                        branches
333                            .get(branch_index)
334                            .map(|branch| (branch_index, branch))
335                    });
336                    let selected_range = selected_branch
337                        .and_then(|(branch_index, _)| branch_child_ranges.get(branch_index))
338                        .cloned()
339                        .unwrap_or(0..0);
340                    let children = borrow!(cloned_self.base().get_instance_children());
341                    let start = selected_range.start.min(children.len());
342                    let end = selected_range.end.min(children.len());
343                    let route_match = selection.route_match.clone();
344                    let env = route_match
345                        .clone()
346                        .map(route_scope)
347                        .map(|scope| cloned_expanded_node.stack.push(scope))
348                        .unwrap_or_else(|| Rc::clone(&cloned_expanded_node.stack));
349                    let selected_range = start..end;
350                    let active_children = borrow!(cloned_expanded_node.active_children).clone();
351                    let selected_children = Self::matching_children_for_range(
352                        &cloned_expanded_node,
353                        &active_children,
354                        &children,
355                        &selected_range,
356                        route_match.as_ref(),
357                        &env,
358                        &cloned_context,
359                        is_mount,
360                    );
361
362                    let mut new_active_children = selected_children;
363                    if selected_branch
364                        .map(|(_, branch)| branch.modal)
365                        .unwrap_or(false)
366                    {
367                        for child in active_children {
368                            if !new_active_children
369                                .iter()
370                                .any(|selected| Rc::ptr_eq(selected, &child))
371                            {
372                                new_active_children.push(child);
373                            }
374                        }
375                    }
376
377                    if is_mount {
378                        cloned_expanded_node.attach_children(
379                            new_active_children,
380                            &cloned_context,
381                            &cloned_expanded_node.parent_frame,
382                        )
383                    } else {
384                        new_active_children
385                    }
386                },
387                &deps,
388                &format!("router_children (node id: {})", expanded_node.id.0),
389            ));
390    }
391
392    fn matching_children_for_range(
393        expanded_node: &Rc<ExpandedNode>,
394        active_children: &[Rc<ExpandedNode>],
395        templates: &[Rc<dyn InstanceNode>],
396        range: &Range<usize>,
397        route_match: Option<&RouteMatch>,
398        env: &Rc<RuntimePropertiesStackFrame>,
399        context: &Rc<RuntimeContext>,
400        is_mount: bool,
401    ) -> Vec<Rc<ExpandedNode>> {
402        let mut selected = Vec::new();
403        for template in templates[range.clone()].iter() {
404            let active = active_children
405                .iter()
406                .find(|child| {
407                    !selected.iter().any(|selected| Rc::ptr_eq(selected, child))
408                        && Self::child_uses_template(child, template)
409                        && Self::same_route_instance(
410                            Self::child_route_match(child).as_ref(),
411                            route_match,
412                        )
413                })
414                .cloned();
415            let rescued = active.or_else(|| {
416                (is_mount && expanded_node.attached.get() > 0)
417                    .then(|| {
418                        expanded_node.rescue_exiting_child_matching(|child| {
419                            Self::child_uses_template(child, template)
420                                && Self::same_route_instance(
421                                    Self::child_route_match(child).as_ref(),
422                                    route_match,
423                                )
424                        })
425                    })
426                    .flatten()
427            });
428            let child = rescued.unwrap_or_else(|| {
429                expanded_node
430                    .create_children_detached(
431                        iter::once((Rc::clone(template), Rc::clone(env))),
432                        context,
433                        &Rc::downgrade(expanded_node),
434                    )
435                    .pop()
436                    .expect("route child creation returned no child")
437            });
438            if let Some(route_match) = route_match {
439                Self::set_child_route_match(&child, route_match.clone());
440            }
441            if !is_mount && child.attached.get() == 0 {
442                child.recurse_control_flow_expansion(context);
443            }
444            selected.push(child);
445        }
446        selected
447    }
448
449    fn child_uses_template(child: &Rc<ExpandedNode>, template: &Rc<dyn InstanceNode>) -> bool {
450        let child_template = borrow!(child.instance_node).clone();
451        Rc::ptr_eq(&child_template, template)
452    }
453
454    fn child_route_match(child: &Rc<ExpandedNode>) -> Option<RouteMatch> {
455        Self::child_route_match_property(child).map(|route_match| route_match.get())
456    }
457
458    fn child_route_match_property(child: &Rc<ExpandedNode>) -> Option<LocalProperty<RouteMatch>> {
459        child
460            .stack
461            .resolve_symbol_as_erased_property(INTERNAL_ROUTE_MATCH_SYMBOL)
462            .map(LocalProperty::<RouteMatch>::new_from_untyped)
463    }
464
465    fn same_route_instance(lhs: Option<&RouteMatch>, rhs: Option<&RouteMatch>) -> bool {
466        // The template identifies the route declaration. Captured params identify its instance;
467        // remainder and location fields are reactive context for that same mounted instance.
468        match (lhs, rhs) {
469            (Some(lhs), Some(rhs)) => lhs.params == rhs.params,
470            (None, None) => true,
471            _ => false,
472        }
473    }
474
475    fn set_child_route_match(child: &Rc<ExpandedNode>, route_match: RouteMatch) {
476        if let Some(property) = Self::child_route_match_property(child) {
477            property.set_if_neq(route_match);
478        }
479    }
480}
481
482pub fn compile_route_branches(
483    definitions: &[ControlFlowRouteBranchDefinition],
484) -> Vec<CompiledRouteBranch> {
485    definitions
486        .iter()
487        .map(CompiledRouteBranch::from_definition)
488        .collect()
489}
490
491fn compile_route_pattern(path: &str) -> Vec<RouteSegmentPattern> {
492    if path.is_empty() {
493        panic!("Route path must not be empty");
494    }
495
496    let normalized = if path == "/" {
497        "/"
498    } else {
499        path.trim_start_matches('/').trim_end_matches('/')
500    };
501
502    if normalized.is_empty() {
503        return vec![];
504    }
505
506    let segments = normalized
507        .split('/')
508        .filter(|segment| !segment.is_empty())
509        .collect::<Vec<_>>();
510    let mut pattern = Vec::new();
511    for (index, segment) in segments.iter().enumerate() {
512        if *segment == "*" {
513            if index != segments.len() - 1 {
514                panic!("Route catch-all '*' must be terminal");
515            }
516            pattern.push(RouteSegmentPattern::CatchAll);
517        } else if let Some(param_name) = segment.strip_prefix(':') {
518            if param_name.is_empty() {
519                panic!("Route params must be named");
520            }
521            pattern.push(RouteSegmentPattern::Param(param_name.to_string()));
522        } else if segment.contains('*') {
523            panic!("Route only supports terminal '*'");
524        } else {
525            pattern.push(RouteSegmentPattern::Literal(segment.to_string()));
526        }
527    }
528
529    pattern
530}
531
532fn select_route_branch(
533    branches: &[CompiledRouteBranch],
534    input: &RouteLocation,
535    global: &RouteLocation,
536) -> RouteSelection {
537    let mut default_index = None;
538
539    for (index, branch) in branches.iter().enumerate() {
540        if branch.default {
541            default_index.get_or_insert(index);
542            continue;
543        }
544
545        if let Some(route_match) = match_route_branch(branch, input, global) {
546            return RouteSelection {
547                branch_index: Some(index),
548                route_match: Some(route_match),
549            };
550        }
551    }
552
553    default_index
554        .map(|index| RouteSelection {
555            branch_index: Some(index),
556            route_match: Some(RouteMatch::default_match(input, global)),
557        })
558        .unwrap_or_default()
559}
560
561fn match_route_branch(
562    branch: &CompiledRouteBranch,
563    input: &RouteLocation,
564    global: &RouteLocation,
565) -> Option<RouteMatch> {
566    let mut params = HashMap::new();
567    let mut input_index = 0usize;
568
569    for (pattern_index, segment_pattern) in branch.pattern.iter().enumerate() {
570        match segment_pattern {
571            RouteSegmentPattern::Literal(expected) => {
572                let actual = input.path_segments.get(input_index)?;
573                if actual != expected {
574                    return None;
575                }
576                input_index += 1;
577            }
578            RouteSegmentPattern::Param(name) => {
579                let actual = input.path_segments.get(input_index)?;
580                params.insert(name.clone(), actual.clone());
581                input_index += 1;
582            }
583            RouteSegmentPattern::CatchAll => {
584                if pattern_index != branch.pattern.len() - 1 {
585                    return None;
586                }
587
588                let remainder = input.path_segments[input_index..].to_vec();
589                return Some(RouteMatch {
590                    location: input.clone(),
591                    global_location: global.clone(),
592                    params,
593                    consumed_segments: input_index,
594                    is_exact: remainder.is_empty(),
595                    remainder,
596                });
597            }
598        }
599    }
600
601    if input_index != input.path_segments.len() {
602        return None;
603    }
604
605    Some(RouteMatch {
606        location: input.clone(),
607        global_location: global.clone(),
608        params,
609        consumed_segments: input_index,
610        remainder: vec![],
611        is_exact: true,
612    })
613}
614
615fn route_scope(route_match: RouteMatch) -> HashMap<String, Variable> {
616    let route_property = LocalProperty::new(route_match);
617    vec![
618        (
619            ROUTE_SYMBOL.to_string(),
620            Variable::new_from_typed_property(route_property.clone()),
621        ),
622        (
623            INTERNAL_ROUTE_MATCH_SYMBOL.to_string(),
624            Variable::new_from_typed_property(route_property),
625        ),
626    ]
627    .into_iter()
628    .collect()
629}
630
631fn multimap_to_pax_value(map: &HashMap<String, Vec<String>>) -> PaxValue {
632    let mut entries = map
633        .iter()
634        .map(|(key, values)| (key.clone(), values.clone().to_pax_value()))
635        .collect::<Vec<_>>();
636    entries.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0));
637    PaxValue::Object(entries)
638}
639
640fn string_map_to_pax_value(map: &HashMap<String, String>) -> PaxValue {
641    let mut entries = map
642        .iter()
643        .map(|(key, value)| (key.clone(), value.clone().to_pax_value()))
644        .collect::<Vec<_>>();
645    entries.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0));
646    PaxValue::Object(entries)
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652    use crate::api::math::{Point2, Transform2};
653    use crate::api::{CommonProperties, Layer, Size, Window};
654    use crate::{
655        BaseInstance, ComponentInstance, Globals, InstanceFlags, RuntimePropertiesStackFrame,
656        TransformAndBounds,
657    };
658    use pax_manifest::cartridge_generation::{
659        ComponentTransitionConfig, TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT,
660        TRANSITION_PHASE_IDLE,
661    };
662    use pax_manifest::ValueDefinition;
663    use pax_runtime_api::pax_value::{PaxAny, ToFromPaxAny};
664    use pax_runtime_api::{Duration, Platform, TargetInfo, OS};
665    use std::cell::RefCell;
666
667    fn route(path_segments: &[&str]) -> RouteLocation {
668        RouteLocation {
669            path_segments: path_segments
670                .iter()
671                .map(|segment| (*segment).to_string())
672                .collect(),
673            ..Default::default()
674        }
675    }
676
677    fn test_globals() -> Globals {
678        Globals {
679            elapsed_frames: LocalProperty::new(0),
680            elapsed_millis: LocalProperty::new(0),
681            viewport: LocalProperty::new(TransformAndBounds {
682                transform: Transform2::identity(),
683                bounds: (100.0, 100.0),
684            }),
685            gyro: LocalProperty::new(Default::default()),
686            accel: LocalProperty::new(Default::default()),
687            route_location: LocalProperty::new(RouteLocation::root()),
688            browser_allows_scroller_vector_layers: LocalProperty::new(true),
689            browser_allows_nested_scroller_vector_layers: LocalProperty::new(true),
690            platform: Platform::Unknown,
691            os: OS::Unknown,
692            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
693            #[cfg(feature = "designtime")]
694            designtime: crate::test_support::designtime(),
695            get_elapsed_millis: Rc::new(|| 0),
696        }
697    }
698
699    fn default_properties_factory() -> Box<
700        dyn Fn(
701            Rc<RuntimePropertiesStackFrame>,
702            Option<Rc<ExpandedNode>>,
703        ) -> Option<Rc<RefCell<PaxAny>>>,
704    > {
705        Box::new(|_, expanded_node| {
706            expanded_node
707                .is_none()
708                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
709        })
710    }
711
712    fn default_common_properties_factory() -> Box<
713        dyn Fn(
714            Rc<RuntimePropertiesStackFrame>,
715            Option<Rc<ExpandedNode>>,
716        ) -> Option<Rc<RefCell<CommonProperties>>>,
717    > {
718        Box::new(|_, expanded_node| {
719            expanded_node
720                .is_none()
721                .then(|| Rc::new(RefCell::new(CommonProperties::default())))
722        })
723    }
724
725    fn component_args(template: Option<Vec<Rc<dyn InstanceNode>>>) -> InstantiationArgs {
726        InstantiationArgs {
727            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
728                default_common_properties_factory(),
729            ),
730            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
731            handler_registry: None,
732            children: None,
733            component_template: template.map(RefCell::new),
734            component_settings: None,
735            template_node_identifier: None,
736            template_node_type_id: None,
737            template_node_selector_info: None,
738            transition_config: Default::default(),
739            properties_scope: crate::PropertiesScopeInit::None,
740        }
741    }
742
743    fn leaf_args() -> InstantiationArgs {
744        InstantiationArgs {
745            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
746                default_common_properties_factory(),
747            ),
748            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
749            handler_registry: None,
750            children: None,
751            component_template: Some(RefCell::new(Vec::new())),
752            component_settings: None,
753            template_node_identifier: None,
754            template_node_type_id: None,
755            template_node_selector_info: None,
756            transition_config: ComponentTransitionConfig {
757                has_enter: true,
758                enter_frame_count: 10,
759                has_exit: true,
760                exit_frame_count: 10,
761                timeout_ms: 5_000,
762                ..Default::default()
763            },
764            properties_scope: crate::PropertiesScopeInit::None,
765        }
766    }
767
768    fn leaf() -> Rc<dyn InstanceNode> {
769        ComponentInstance::instantiate(leaf_args())
770    }
771
772    struct HitBoxInstance {
773        base: BaseInstance,
774    }
775
776    impl InstanceNode for HitBoxInstance {
777        fn base(&self) -> &BaseInstance {
778            &self.base
779        }
780
781        fn instantiate(args: InstantiationArgs) -> Rc<Self> {
782            Rc::new(Self {
783                base: BaseInstance::new(
784                    args,
785                    InstanceFlags {
786                        invisible_to_slot: false,
787                        invisible_to_raycasting: false,
788                        layer: Layer::Canvas,
789                        is_component: false,
790                        is_slot: false,
791                    },
792                ),
793            })
794        }
795
796        fn resolve_debug(
797            &self,
798            f: &mut std::fmt::Formatter,
799            _expanded_node: Option<&ExpandedNode>,
800        ) -> std::fmt::Result {
801            write!(f, "HitBox")
802        }
803    }
804
805    fn hit_box_args(transition_config: ComponentTransitionConfig) -> InstantiationArgs {
806        InstantiationArgs {
807            prototypical_common_properties: crate::CommonPropertiesInit::Factory(Box::new(
808                |_, expanded_node| {
809                    expanded_node.is_none().then(|| {
810                        let mut cp = CommonProperties::default();
811                        cp.width = LocalProperty::new(Some(Size::Pixels(40.into())));
812                        cp.height = LocalProperty::new(Some(Size::Pixels(40.into())));
813                        Rc::new(RefCell::new(cp))
814                    })
815                },
816            )),
817            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
818            handler_registry: None,
819            children: None,
820            component_template: Some(RefCell::new(Vec::new())),
821            component_settings: None,
822            template_node_identifier: None,
823            template_node_type_id: None,
824            template_node_selector_info: None,
825            transition_config,
826            properties_scope: crate::PropertiesScopeInit::None,
827        }
828    }
829
830    fn hit_box(transition_config: ComponentTransitionConfig) -> Rc<dyn InstanceNode> {
831        HitBoxInstance::instantiate(hit_box_args(transition_config))
832    }
833
834    fn dynamic_exit_duration_leaf(frames: f64) -> Rc<dyn InstanceNode> {
835        let mut args = leaf_args();
836        args.transition_config = ComponentTransitionConfig {
837            has_exit: true,
838            exit_dynamic_durations: vec![ValueDefinition::LiteralValue(PaxValue::Duration(
839                Duration::Frames(frames.into()),
840            ))],
841            timeout_ms: 5_000,
842            ..Default::default()
843        };
844        ComponentInstance::instantiate(args)
845    }
846
847    fn router_args(
848        input_location: LocalProperty<RouteLocation>,
849        global_location: LocalProperty<RouteLocation>,
850        children: Vec<Rc<dyn InstanceNode>>,
851    ) -> InstantiationArgs {
852        let input_location_for_factory = input_location.clone();
853        let global_location_for_factory = global_location.clone();
854        InstantiationArgs {
855            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
856                default_common_properties_factory(),
857            ),
858            prototypical_properties: crate::PropertiesInit::Factory(Box::new(
859                move |_, expanded_node| {
860                    if let Some(expanded_node) = expanded_node {
861                        expanded_node.with_properties_unwrapped(
862                            |properties: &mut RouterProperties| {
863                                properties
864                                    .input_location
865                                    .replace_with(input_location_for_factory.clone());
866                                properties
867                                    .global_location
868                                    .replace_with(global_location_for_factory.clone());
869                            },
870                        );
871                        return None;
872                    }
873
874                    Some(Rc::new(RefCell::new({
875                        let mut properties = RouterProperties::default();
876                        properties.input_location = input_location_for_factory.clone();
877                        properties.global_location = global_location_for_factory.clone();
878                        properties.to_pax_any()
879                    })))
880                },
881            )),
882            handler_registry: None,
883            children: Some(RefCell::new(children)),
884            component_template: None,
885            component_settings: None,
886            template_node_identifier: None,
887            template_node_type_id: None,
888            template_node_selector_info: None,
889            transition_config: Default::default(),
890            properties_scope: crate::PropertiesScopeInit::None,
891        }
892    }
893
894    fn mounted_router(
895        input_location: LocalProperty<RouteLocation>,
896        branch_definitions: Vec<ControlFlowRouteBranchDefinition>,
897        branch_child_ranges: Vec<Range<usize>>,
898        children: Vec<Rc<dyn InstanceNode>>,
899    ) -> (Rc<ExpandedNode>, Rc<ExpandedNode>, Rc<RuntimeContext>) {
900        let router: Rc<dyn InstanceNode> = RouterInstance::instantiate_with_branches(
901            router_args(input_location.clone(), input_location, children),
902            compile_route_branches(&branch_definitions),
903            branch_child_ranges,
904        );
905        let root_component =
906            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&router)])));
907        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
908        let root = ExpandedNode::initialize_root(root_component, &context);
909        root.recurse_update(&context);
910        let router_node = root.children.get().remove(0);
911        (root, router_node, context)
912    }
913
914    fn route_param(node: &Rc<ExpandedNode>, name: &str) -> Option<String> {
915        let route = node.stack.resolve_symbol(ROUTE_SYMBOL)?.get_as_pax_value();
916        let PaxValue::Object(route_fields) = route else {
917            return None;
918        };
919        let params = route_fields
920            .into_iter()
921            .find_map(|(field, value)| (field == "params").then_some(value))?;
922        let PaxValue::Object(param_fields) = params else {
923            return None;
924        };
925        param_fields
926            .into_iter()
927            .find_map(|(field, value)| (field == name).then_some(value))
928            .and_then(|value| match value {
929                PaxValue::String(value) => Some(value),
930                _ => None,
931            })
932    }
933
934    #[test]
935    fn terminal_catch_all_preserves_remainder() {
936        let branches = compile_route_branches(&[ControlFlowRouteBranchDefinition {
937            path: Some("/settings/*".to_string()),
938            default: false,
939            modal: false,
940            child_ids: vec![],
941            metadata: None,
942        }]);
943        let input = route(&["settings", "team", "42"]);
944        let selection = select_route_branch(&branches, &input, &input);
945
946        assert_eq!(selection.branch_index, Some(0));
947        let route_match = selection.route_match.unwrap();
948        assert_eq!(route_match.consumed_segments, 1);
949        assert_eq!(
950            route_match.remainder,
951            vec!["team".to_string(), "42".to_string()]
952        );
953        assert!(!route_match.is_exact);
954    }
955
956    #[test]
957    fn params_are_captured_for_exact_routes() {
958        let branches = compile_route_branches(&[ControlFlowRouteBranchDefinition {
959            path: Some("/docs/:slug".to_string()),
960            default: false,
961            modal: false,
962            child_ids: vec![],
963            metadata: None,
964        }]);
965        let input = route(&["docs", "router"]);
966        let selection = select_route_branch(&branches, &input, &input);
967
968        assert_eq!(selection.branch_index, Some(0));
969        let route_match = selection.route_match.unwrap();
970        assert_eq!(route_match.params.get("slug"), Some(&"router".to_string()));
971        assert_eq!(route_match.remainder, Vec::<String>::new());
972        assert!(route_match.is_exact);
973    }
974
975    #[test]
976    fn relative_paths_compile_like_absolute_paths() {
977        let branches = compile_route_branches(&[ControlFlowRouteBranchDefinition {
978            path: Some("settings/*".to_string()),
979            default: false,
980            modal: false,
981            child_ids: vec![],
982            metadata: None,
983        }]);
984        let input = route(&["settings", "integrations", "logs"]);
985        let selection = select_route_branch(&branches, &input, &input);
986
987        assert_eq!(selection.branch_index, Some(0));
988        let route_match = selection.route_match.unwrap();
989        assert_eq!(
990            route_match.remainder,
991            vec!["integrations".to_string(), "logs".to_string()]
992        );
993    }
994
995    #[test]
996    fn default_branch_receives_unmatched_input() {
997        let branches = compile_route_branches(&[
998            ControlFlowRouteBranchDefinition {
999                path: Some("/docs/:slug".to_string()),
1000                default: false,
1001                modal: false,
1002                child_ids: vec![],
1003                metadata: None,
1004            },
1005            ControlFlowRouteBranchDefinition {
1006                path: None,
1007                default: true,
1008                modal: false,
1009                child_ids: vec![],
1010                metadata: None,
1011            },
1012        ]);
1013        let input = route(&["missing"]);
1014        let selection = select_route_branch(&branches, &input, &input);
1015
1016        assert_eq!(selection.branch_index, Some(1));
1017        let route_match = selection.route_match.unwrap();
1018        assert_eq!(route_match.remainder, vec!["missing".to_string()]);
1019        assert_eq!(route_match.location, input);
1020    }
1021
1022    #[test]
1023    fn same_branch_route_match_change_enters_new_tree_and_exits_old_tree() {
1024        let input_location = LocalProperty::new(route(&["users", "1"]));
1025        let (_root, router_node, context) = mounted_router(
1026            input_location.clone(),
1027            vec![ControlFlowRouteBranchDefinition {
1028                path: Some("/users/:id".to_string()),
1029                default: false,
1030                modal: false,
1031                child_ids: vec![],
1032                metadata: None,
1033            }],
1034            vec![0..1],
1035            vec![leaf()],
1036        );
1037
1038        let initial_active = borrow!(router_node.active_children).clone();
1039        assert_eq!(initial_active.len(), 1);
1040        assert_eq!(route_param(&initial_active[0], "id"), Some("1".to_string()));
1041
1042        input_location.set(route(&["users", "2"]));
1043        router_node.recurse_update(&context);
1044
1045        let active = borrow!(router_node.active_children).clone();
1046        let exiting = borrow!(router_node.exiting_children).clone();
1047        assert_eq!(active.len(), 1);
1048        assert_eq!(exiting.len(), 1);
1049        assert_ne!(active[0].id.0, initial_active[0].id.0);
1050        assert_eq!(route_param(&active[0], "id"), Some("2".to_string()));
1051        assert_eq!(route_param(&exiting[0], "id"), Some("1".to_string()));
1052        assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1053        assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
1054    }
1055
1056    #[test]
1057    fn catch_all_reuses_route_tree_when_only_remainder_changes() {
1058        let input_location = LocalProperty::new(route(&["teams", "atlas"]));
1059        let (_root, router_node, context) = mounted_router(
1060            input_location.clone(),
1061            vec![ControlFlowRouteBranchDefinition {
1062                path: Some("/teams/:id/*".to_string()),
1063                default: false,
1064                modal: false,
1065                child_ids: vec![],
1066                metadata: None,
1067            }],
1068            vec![0..1],
1069            vec![leaf()],
1070        );
1071
1072        let initial_active = borrow!(router_node.active_children).clone();
1073        assert_eq!(initial_active.len(), 1);
1074
1075        input_location.set(route(&[
1076            "teams",
1077            "atlas",
1078            "settings",
1079            "integrations",
1080            "logs",
1081        ]));
1082        router_node.recurse_update(&context);
1083
1084        let active = borrow!(router_node.active_children).clone();
1085        assert_eq!(active.len(), 1);
1086        assert_eq!(active[0].id.0, initial_active[0].id.0);
1087        assert!(borrow!(router_node.exiting_children).is_empty());
1088        assert_eq!(
1089            RouterInstance::child_route_match(&active[0])
1090                .expect("active route should retain its match")
1091                .remainder,
1092            vec![
1093                "settings".to_string(),
1094                "integrations".to_string(),
1095                "logs".to_string(),
1096            ]
1097        );
1098    }
1099
1100    #[test]
1101    fn branch_change_transitions_each_root_of_multi_root_routes() {
1102        let input_location = LocalProperty::new(route(&["alpha"]));
1103        let (_root, router_node, context) = mounted_router(
1104            input_location.clone(),
1105            vec![
1106                ControlFlowRouteBranchDefinition {
1107                    path: Some("/alpha".to_string()),
1108                    default: false,
1109                    modal: false,
1110                    child_ids: vec![],
1111                    metadata: None,
1112                },
1113                ControlFlowRouteBranchDefinition {
1114                    path: Some("/beta".to_string()),
1115                    default: false,
1116                    modal: false,
1117                    child_ids: vec![],
1118                    metadata: None,
1119                },
1120            ],
1121            vec![0..2, 2..4],
1122            vec![leaf(), leaf(), leaf(), leaf()],
1123        );
1124
1125        let initial_active = borrow!(router_node.active_children).clone();
1126        assert_eq!(initial_active.len(), 2);
1127
1128        input_location.set(route(&["beta"]));
1129        router_node.recurse_update(&context);
1130
1131        let active = borrow!(router_node.active_children).clone();
1132        let exiting = borrow!(router_node.exiting_children).clone();
1133        assert_eq!(active.len(), 2);
1134        assert_eq!(exiting.len(), 2);
1135        assert!(active
1136            .iter()
1137            .all(|child| child.transition_phase.get() == TRANSITION_PHASE_ENTER));
1138        assert!(exiting
1139            .iter()
1140            .all(|child| child.transition_phase.get() == TRANSITION_PHASE_EXIT));
1141        assert!(initial_active
1142            .iter()
1143            .all(|old_child| exiting.iter().any(|child| child.id.0 == old_child.id.0)));
1144    }
1145
1146    #[test]
1147    fn branch_can_be_reselected_while_previous_instance_is_exiting() {
1148        let input_location = LocalProperty::new(route(&["alpha"]));
1149        let (_root, router_node, context) = mounted_router(
1150            input_location.clone(),
1151            vec![
1152                ControlFlowRouteBranchDefinition {
1153                    path: Some("/alpha".to_string()),
1154                    default: false,
1155                    modal: false,
1156                    child_ids: vec![],
1157                    metadata: None,
1158                },
1159                ControlFlowRouteBranchDefinition {
1160                    path: Some("/beta".to_string()),
1161                    default: false,
1162                    modal: false,
1163                    child_ids: vec![],
1164                    metadata: None,
1165                },
1166            ],
1167            vec![0..1, 1..2],
1168            vec![leaf(), leaf()],
1169        );
1170
1171        let initial_alpha = borrow!(router_node.active_children)[0].id;
1172
1173        input_location.set(route(&["beta"]));
1174        router_node.recurse_update(&context);
1175        assert_eq!(borrow!(router_node.active_children).len(), 1);
1176        assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1177        assert_eq!(borrow!(router_node.exiting_children)[0].id, initial_alpha);
1178
1179        input_location.set(route(&["alpha"]));
1180        router_node.recurse_update(&context);
1181
1182        let active = borrow!(router_node.active_children).clone();
1183        let exiting = borrow!(router_node.exiting_children).clone();
1184        assert_eq!(active.len(), 1);
1185        assert_eq!(active[0].id, initial_alpha);
1186        assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1187        assert_eq!(exiting.len(), 1);
1188        assert_ne!(exiting[0].id, initial_alpha);
1189    }
1190
1191    #[test]
1192    fn retained_exiting_route_tree_does_not_intercept_hit_testing() {
1193        let input_location = LocalProperty::new(route(&["alpha"]));
1194        let exiting_route_config = ComponentTransitionConfig {
1195            has_exit: true,
1196            exit_frame_count: 30,
1197            timeout_ms: 5_000,
1198            ..Default::default()
1199        };
1200        let (root, router_node, context) = mounted_router(
1201            input_location.clone(),
1202            vec![
1203                ControlFlowRouteBranchDefinition {
1204                    path: Some("/alpha".to_string()),
1205                    default: false,
1206                    modal: false,
1207                    child_ids: vec![],
1208                    metadata: None,
1209                },
1210                ControlFlowRouteBranchDefinition {
1211                    path: Some("/beta".to_string()),
1212                    default: false,
1213                    modal: false,
1214                    child_ids: vec![],
1215                    metadata: None,
1216                },
1217            ],
1218            vec![0..1, 1..2],
1219            vec![hit_box(exiting_route_config), hit_box(Default::default())],
1220        );
1221
1222        input_location.set(route(&["beta"]));
1223        router_node.recurse_update(&context);
1224
1225        let active = borrow!(router_node.active_children).clone();
1226        assert_eq!(active.len(), 1);
1227        assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1228
1229        let hits = context.get_elements_beneath_ray(
1230            Some(root),
1231            Point2::<Window>::new(5.0, 5.0),
1232            true,
1233            vec![],
1234            false,
1235        );
1236        assert_eq!(hits.first().map(|node| node.id), Some(active[0].id));
1237    }
1238
1239    #[test]
1240    fn dynamic_exit_duration_controls_retained_route_cleanup() {
1241        let input_location = LocalProperty::new(route(&["alpha"]));
1242        let (_root, router_node, context) = mounted_router(
1243            input_location.clone(),
1244            vec![
1245                ControlFlowRouteBranchDefinition {
1246                    path: Some("/alpha".to_string()),
1247                    default: false,
1248                    modal: false,
1249                    child_ids: vec![],
1250                    metadata: None,
1251                },
1252                ControlFlowRouteBranchDefinition {
1253                    path: Some("/beta".to_string()),
1254                    default: false,
1255                    modal: false,
1256                    child_ids: vec![],
1257                    metadata: None,
1258                },
1259            ],
1260            vec![0..1, 1..2],
1261            vec![dynamic_exit_duration_leaf(3.0), leaf()],
1262        );
1263
1264        input_location.set(route(&["beta"]));
1265        router_node.recurse_update(&context);
1266        assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1267        let exiting_child = borrow!(router_node.exiting_children)[0].clone();
1268
1269        context.globals().elapsed_frames.set(2);
1270        context.drain_node_effects();
1271        assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1272
1273        context.globals().elapsed_frames.set(3);
1274        context.drain_node_effects();
1275        assert!(borrow!(router_node.exiting_children).is_empty());
1276        assert_eq!(exiting_child.transition_phase.get(), TRANSITION_PHASE_IDLE);
1277        assert!(!pax_runtime_api::properties::property_has_direct_outbound(
1278            &context.globals().elapsed_frames.untyped(),
1279            &exiting_child.transition_playhead.untyped(),
1280        ));
1281        assert!(!pax_runtime_api::properties::property_has_direct_outbound(
1282            &context.globals().elapsed_millis.untyped(),
1283            &exiting_child.transition_playhead_millis.untyped(),
1284        ));
1285    }
1286
1287    #[test]
1288    fn active_enter_transition_returns_to_idle_after_duration() {
1289        let input_location = LocalProperty::new(route(&["alpha"]));
1290        let (_root, router_node, context) = mounted_router(
1291            input_location.clone(),
1292            vec![
1293                ControlFlowRouteBranchDefinition {
1294                    path: Some("/alpha".to_string()),
1295                    default: false,
1296                    modal: false,
1297                    child_ids: vec![],
1298                    metadata: None,
1299                },
1300                ControlFlowRouteBranchDefinition {
1301                    path: Some("/beta".to_string()),
1302                    default: false,
1303                    modal: false,
1304                    child_ids: vec![],
1305                    metadata: None,
1306                },
1307            ],
1308            vec![0..1, 1..2],
1309            vec![leaf(), leaf()],
1310        );
1311
1312        input_location.set(route(&["beta"]));
1313        router_node.recurse_update(&context);
1314
1315        let active = borrow!(router_node.active_children).clone();
1316        assert_eq!(active.len(), 1);
1317        assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1318
1319        context.globals().elapsed_frames.set(9);
1320        context.drain_node_effects();
1321        assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1322
1323        context.globals().elapsed_frames.set(10);
1324        context.drain_node_effects();
1325        assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_IDLE);
1326    }
1327
1328    #[test]
1329    fn modal_branch_keeps_previous_route_active_underneath() {
1330        let input_location = LocalProperty::new(route(&["alpha"]));
1331        let (_root, router_node, context) = mounted_router(
1332            input_location.clone(),
1333            vec![
1334                ControlFlowRouteBranchDefinition {
1335                    path: Some("/alpha".to_string()),
1336                    default: false,
1337                    modal: false,
1338                    child_ids: vec![],
1339                    metadata: None,
1340                },
1341                ControlFlowRouteBranchDefinition {
1342                    path: Some("/tools".to_string()),
1343                    default: false,
1344                    modal: true,
1345                    child_ids: vec![],
1346                    metadata: None,
1347                },
1348            ],
1349            vec![0..1, 1..2],
1350            vec![leaf(), leaf()],
1351        );
1352
1353        let base_id = borrow!(router_node.active_children)[0].id;
1354
1355        input_location.set(route(&["tools"]));
1356        router_node.recurse_update(&context);
1357
1358        let active = borrow!(router_node.active_children).clone();
1359        let exiting = borrow!(router_node.exiting_children).clone();
1360        assert_eq!(active.len(), 2);
1361        assert!(exiting.is_empty());
1362        assert_ne!(active[0].id, base_id);
1363        assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1364        assert_eq!(active[1].id, base_id);
1365    }
1366
1367    #[test]
1368    fn dismissing_modal_reveals_previous_route_without_remounting() {
1369        let input_location = LocalProperty::new(route(&["alpha"]));
1370        let (_root, router_node, context) = mounted_router(
1371            input_location.clone(),
1372            vec![
1373                ControlFlowRouteBranchDefinition {
1374                    path: Some("/alpha".to_string()),
1375                    default: false,
1376                    modal: false,
1377                    child_ids: vec![],
1378                    metadata: None,
1379                },
1380                ControlFlowRouteBranchDefinition {
1381                    path: Some("/tools".to_string()),
1382                    default: false,
1383                    modal: true,
1384                    child_ids: vec![],
1385                    metadata: None,
1386                },
1387            ],
1388            vec![0..1, 1..2],
1389            vec![leaf(), leaf()],
1390        );
1391
1392        let base_id = borrow!(router_node.active_children)[0].id;
1393
1394        input_location.set(route(&["tools"]));
1395        router_node.recurse_update(&context);
1396        let modal_id = borrow!(router_node.active_children)[0].id;
1397
1398        input_location.set(route(&["alpha"]));
1399        router_node.recurse_update(&context);
1400
1401        let active = borrow!(router_node.active_children).clone();
1402        let exiting = borrow!(router_node.exiting_children).clone();
1403        assert_eq!(active.len(), 1);
1404        assert_eq!(active[0].id, base_id);
1405        assert_eq!(exiting.len(), 1);
1406        assert_eq!(exiting[0].id, modal_id);
1407        assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
1408    }
1409
1410    #[test]
1411    fn no_match_transitions_out_previous_route_tree() {
1412        let input_location = LocalProperty::new(route(&["alpha"]));
1413        let (_root, router_node, context) = mounted_router(
1414            input_location.clone(),
1415            vec![ControlFlowRouteBranchDefinition {
1416                path: Some("/alpha".to_string()),
1417                default: false,
1418                modal: false,
1419                child_ids: vec![],
1420                metadata: None,
1421            }],
1422            vec![0..1],
1423            vec![leaf()],
1424        );
1425
1426        assert_eq!(borrow!(router_node.active_children).len(), 1);
1427
1428        input_location.set(route(&["missing"]));
1429        router_node.recurse_update(&context);
1430
1431        let active = borrow!(router_node.active_children).clone();
1432        let exiting = borrow!(router_node.exiting_children).clone();
1433        assert!(active.is_empty());
1434        assert_eq!(exiting.len(), 1);
1435        assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
1436    }
1437}