Skip to main content

hypen_engine/ir/
component.rs

1use super::Element;
2use indexmap::IndexMap;
3use std::sync::Arc;
4
5/// Result from component resolution
6/// Contains the source code and the resolved path for the component
7pub struct ResolvedComponent {
8    pub source: String,
9    pub path: String,
10    pub passthrough: bool,
11    pub lazy: bool,
12}
13
14/// Callback type for resolving component source code
15/// Takes component name and optional path context, returns resolved component
16/// Path context is the file path where this component is being referenced from
17/// Resolver should return (source_code, resolved_path) where resolved_path is the
18/// absolute path to the component file (used for resolving nested components)
19pub type ComponentResolver =
20    Arc<dyn Fn(&str, Option<&str>) -> Option<ResolvedComponent> + Send + Sync>;
21
22/// A component definition - a template that can be instantiated
23#[derive(Clone)]
24pub struct Component {
25    /// Component name
26    pub name: String,
27
28    /// Template function that produces element tree
29    /// Takes props and returns expanded element tree
30    pub template: Arc<dyn Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync>,
31
32    /// Default props
33    pub default_props: IndexMap<String, serde_json::Value>,
34
35    /// Source path where this component was loaded from (optional)
36    pub source_path: Option<String>,
37
38    /// If true, this component acts as a passthrough container
39    /// It preserves its children and props without template expansion
40    pub passthrough: bool,
41
42    /// If true, this component's children are NOT expanded during initial pass
43    /// Children remain as component references for lazy rendering
44    pub lazy: bool,
45}
46
47impl Component {
48    pub fn new(
49        name: impl Into<String>,
50        template: impl Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync + 'static,
51    ) -> Self {
52        Self {
53            name: name.into(),
54            template: Arc::new(template),
55            default_props: IndexMap::new(),
56            source_path: None,
57            passthrough: false,
58            lazy: false,
59        }
60    }
61
62    pub fn with_defaults(mut self, defaults: IndexMap<String, serde_json::Value>) -> Self {
63        self.default_props = defaults;
64        self
65    }
66
67    pub fn with_source_path(mut self, path: impl Into<String>) -> Self {
68        self.source_path = Some(path.into());
69        self
70    }
71
72    pub fn with_passthrough(mut self, passthrough: bool) -> Self {
73        self.passthrough = passthrough;
74        self
75    }
76
77    pub fn with_lazy(mut self, lazy: bool) -> Self {
78        self.lazy = lazy;
79        self
80    }
81
82    /// Instantiate this component with given props
83    pub fn instantiate(&self, props: IndexMap<String, serde_json::Value>) -> Element {
84        let mut merged_props = self.default_props.clone();
85        merged_props.extend(props);
86        (self.template)(merged_props)
87    }
88}
89
90/// Registry of all available components
91pub struct ComponentRegistry {
92    /// Components indexed by fully qualified key (path:name or just name)
93    components: IndexMap<String, Component>,
94    /// Optional resolver for dynamically loading components
95    resolver: Option<ComponentResolver>,
96    /// Cache of resolved component paths to prevent re-resolving
97    /// Key format: "path:component_name" or "component_name" if no path
98    resolved_cache: IndexMap<String, bool>,
99}
100
101impl ComponentRegistry {
102    pub fn new() -> Self {
103        Self {
104            components: IndexMap::new(),
105            resolver: None,
106            resolved_cache: IndexMap::new(),
107        }
108    }
109
110    /// Register a primitive element name to skip component resolution
111    /// Called by the renderer to mark built-in DOM elements
112    pub fn register_primitive(&mut self, name: &str) {
113        self.resolved_cache.insert(name.to_string(), false);
114    }
115
116    /// Set the component resolver callback
117    pub fn set_resolver(&mut self, resolver: ComponentResolver) {
118        self.resolver = Some(resolver);
119    }
120
121    pub fn register(&mut self, component: Component) {
122        // Register with qualified key if source path is available
123        if let Some(ref path) = component.source_path {
124            let qualified_key = format!("{}:{}", path, component.name);
125            self.components.insert(qualified_key, component.clone());
126        }
127
128        // Always register with unqualified name as fallback
129        self.components.insert(component.name.clone(), component);
130    }
131
132    /// Get a component by name and optional context path
133    pub fn get(&self, name: &str, context_path: Option<&str>) -> Option<&Component> {
134        // Try with context path first
135        if let Some(path) = context_path {
136            let qualified_key = format!("{}:{}", path, name);
137            if let Some(component) = self.components.get(&qualified_key) {
138                return Some(component);
139            }
140        }
141
142        // Fall back to just name (for globally registered components)
143        self.components.get(name)
144    }
145
146    /// Try to resolve and register a component by name and context path
147    fn try_resolve(&mut self, name: &str, context_path: Option<&str>) -> bool {
148        // Build cache key
149        let cache_key = if let Some(path) = context_path {
150            format!("{}:{}", path, name)
151        } else {
152            name.to_string()
153        };
154
155        // Check cache first
156        if let Some(&cached) = self.resolved_cache.get(&cache_key) {
157            return cached;
158        }
159
160        // Try to resolve
161        if let Some(ref resolver) = self.resolver {
162            if let Some(resolved) = resolver(name, context_path) {
163                // For lazy components, don't parse the template
164                // Children won't be expanded until explicitly requested
165                if resolved.lazy {
166                    #[cfg(all(target_arch = "wasm32", feature = "js"))]
167                    web_sys::console::log_1(
168                        &format!("Registering lazy component: {}", name).into(),
169                    );
170
171                    // Create a dummy component - children won't be expanded
172                    let dummy_element = Element::new(name);
173                    let component = Component::new(name, move |_props| dummy_element.clone())
174                        .with_source_path(resolved.path.clone())
175                        .with_lazy(true);
176
177                    self.register(component);
178                    self.resolved_cache.insert(cache_key, true);
179                    return true;
180                }
181
182                // For passthrough components, don't parse the template
183                // They act as transparent containers
184                if resolved.passthrough {
185                    #[cfg(all(target_arch = "wasm32", feature = "js"))]
186                    web_sys::console::log_1(
187                        &format!("Registering passthrough component: {}", name).into(),
188                    );
189
190                    // Create a dummy component - the template won't be used for passthrough
191                    let dummy_element = Element::new(name);
192                    let component = Component::new(name, move |_props| dummy_element.clone())
193                        .with_source_path(resolved.path.clone())
194                        .with_passthrough(true);
195
196                    self.register(component);
197                    self.resolved_cache.insert(cache_key, true);
198                    return true;
199                }
200
201                // Parse the component source for non-passthrough components
202                match hypen_parser::parse_component(&resolved.source) {
203                    Ok(component_spec) => {
204                        // Convert to IR, preserving control-flow children
205                        let ir_node = super::expand::ast_to_ir_node(&component_spec);
206                        let element = match ir_node {
207                            super::IRNode::Element(e) => e,
208                            _ => {
209                                #[cfg(all(target_arch = "wasm32", feature = "js"))]
210                                web_sys::console::error_1(
211                                    &format!("Component {} root must be an element", name).into(),
212                                );
213                                return false;
214                            }
215                        };
216
217                        // Create a component that returns the parsed element
218                        let component = Component::new(name, move |_props| element.clone())
219                            .with_source_path(resolved.path.clone())
220                            .with_passthrough(false);
221
222                        self.register(component);
223                        self.resolved_cache.insert(cache_key, true);
224                        return true;
225                    }
226                    Err(e) => {
227                        #[cfg(all(target_arch = "wasm32", feature = "js"))]
228                        web_sys::console::error_1(
229                            &format!("Failed to parse component {}: {:?}", name, e).into(),
230                        );
231
232                        #[cfg(not(all(target_arch = "wasm32", feature = "js")))]
233                        eprintln!("Failed to parse component {}: {:?}", name, e);
234
235                        self.resolved_cache.insert(cache_key, false);
236                        return false;
237                    }
238                }
239            }
240        }
241
242        self.resolved_cache.insert(cache_key, false);
243        false
244    }
245
246    pub fn expand(&mut self, element: &Element) -> Element {
247        self.expand_with_context(element, None)
248    }
249
250    /// Force expand an element's children (used for lazy components)
251    /// This will expand children that were previously kept unexpanded
252    pub fn expand_children(
253        &mut self,
254        element: &Element,
255        context_path: Option<&str>,
256    ) -> Vec<Element> {
257        element
258            .children
259            .iter()
260            .map(|child| self.expand_with_context(child, context_path))
261            .collect()
262    }
263
264    /// Expand an element with a context path for component resolution
265    fn expand_with_context(&mut self, element: &Element, context_path: Option<&str>) -> Element {
266        // First check if component exists, if not try to resolve it
267        let component_exists = self.get(&element.element_type, context_path).is_some();
268
269        if !component_exists {
270            // Try to resolve the component dynamically
271            self.try_resolve(&element.element_type, context_path);
272        }
273
274        // If this element references a registered component, expand it
275        if let Some(component) = self.get(&element.element_type, context_path) {
276            // Check if this is a lazy component (children NOT expanded until explicitly requested)
277            if component.lazy {
278                // Lazy component: keep element and children, but DON'T expand children yet
279                let mut element = element.clone();
280
281                // Mark as lazy so reconciler knows to skip children
282                element.props.insert(
283                    "__lazy".to_string(),
284                    super::Value::Static(serde_json::json!(true)),
285                );
286
287                #[cfg(all(target_arch = "wasm32", feature = "js"))]
288                web_sys::console::log_1(
289                    &format!(
290                        "Lazy {} (props: {:?}): {} children kept unexpanded",
291                        element.element_type,
292                        element.props.keys().collect::<Vec<_>>(),
293                        element.children.len()
294                    )
295                    .into(),
296                );
297
298                return element;
299            }
300
301            // Check if this is a passthrough component
302            if component.passthrough {
303                // Passthrough component: keep the original element but expand its children
304                let mut element = element.clone();
305
306                #[cfg(all(target_arch = "wasm32", feature = "js"))]
307                {
308                    let props_str = element
309                        .props
310                        .iter()
311                        .map(|(k, v)| format!("{}={:?}", k, v))
312                        .collect::<Vec<_>>()
313                        .join(", ");
314                    web_sys::console::log_1(
315                        &format!(
316                            "Passthrough {} (props: [{}]): {} children before expansion",
317                            element.element_type,
318                            props_str,
319                            element.children.len()
320                        )
321                        .into(),
322                    );
323                }
324
325                // Get the source path for child context
326                let child_context = component.source_path.clone();
327                let child_context_ref = child_context.as_deref();
328
329                // Recursively expand children
330                element.children = element
331                    .children
332                    .into_iter()
333                    .map(|child| Arc::new(self.expand_with_context(&child, child_context_ref)))
334                    .collect();
335
336                #[cfg(all(target_arch = "wasm32", feature = "js"))]
337                web_sys::console::log_1(
338                    &format!(
339                        "Passthrough {}: {} children after expansion",
340                        element.element_type,
341                        element.children.len()
342                    )
343                    .into(),
344                );
345
346                element
347            } else {
348                // Regular component: instantiate template and replace
349                // Convert Value props to serde_json::Value (resolve only static values here)
350                let mut props = IndexMap::new();
351                for (k, v) in &element.props {
352                    if let super::Value::Static(val) = v {
353                        props.insert(k.clone(), val.clone());
354                    }
355                }
356
357                let mut expanded = component.instantiate(props);
358
359                // Preserve bindings and actions from the original element
360                for (k, v) in &element.props {
361                    match v {
362                        super::Value::Binding(_) | super::Value::Action(_) => {
363                            expanded.props.insert(k.clone(), v.clone());
364                        }
365                        _ => {}
366                    }
367                }
368
369                // Get the source path of this component for resolving its children
370                // Clone it to avoid holding a borrow
371                let child_context = component.source_path.clone();
372
373                // Replace Children() placeholders with actual children from the caller
374                expanded.children = self.replace_children_slots(
375                    &expanded.children,
376                    &element.children,
377                    context_path,
378                );
379
380                // Recursively expand children with the new context
381                let child_context_ref = child_context.as_deref();
382                expanded.children = expanded
383                    .children
384                    .into_iter()
385                    .map(|child| Arc::new(self.expand_with_context(&child, child_context_ref)))
386                    .collect();
387                // Also expand ir_children (control-flow children from ast_to_ir_node)
388                if !expanded.ir_children.is_empty() {
389                    expanded.ir_children = expanded
390                        .ir_children
391                        .iter()
392                        .map(|child| self.expand_ir_node_with_context(child, child_context_ref))
393                        .collect();
394                }
395
396                expanded
397            }
398        } else {
399            // Not a component, just expand children
400            let mut element = element.clone();
401            element.children = element
402                .children
403                .into_iter()
404                .map(|child| Arc::new(self.expand_with_context(&child, context_path)))
405                .collect();
406            // Also expand ir_children (control-flow children from ast_to_ir_node)
407            if !element.ir_children.is_empty() {
408                element.ir_children = element
409                    .ir_children
410                    .iter()
411                    .map(|child| self.expand_ir_node_with_context(child, context_path))
412                    .collect();
413            }
414            element
415        }
416    }
417
418    /// Expand an IRNode recursively, expanding any Element nodes via the component registry
419    pub fn expand_ir_node(&mut self, node: &super::IRNode) -> super::IRNode {
420        self.expand_ir_node_with_context(node, None)
421    }
422
423    /// Expand an IRNode with context path for component resolution
424    fn expand_ir_node_with_context(
425        &mut self,
426        node: &super::IRNode,
427        context_path: Option<&str>,
428    ) -> super::IRNode {
429        match node {
430            super::IRNode::Element(element) => {
431                // Expand the element through the registry
432                let mut expanded = self.expand_with_context(element, context_path);
433
434                // Also expand ir_children if present (control-flow children)
435                if !expanded.ir_children.is_empty() {
436                    expanded.ir_children = expanded
437                        .ir_children
438                        .iter()
439                        .map(|child| self.expand_ir_node_with_context(child, context_path))
440                        .collect();
441                }
442
443                super::IRNode::Element(expanded)
444            }
445            super::IRNode::ForEach {
446                source,
447                item_name,
448                key_path,
449                template,
450                props,
451            } => {
452                // Recursively expand template children
453                let expanded_template: Vec<super::IRNode> = template
454                    .iter()
455                    .map(|child| self.expand_ir_node_with_context(child, context_path))
456                    .collect();
457
458                super::IRNode::ForEach {
459                    source: source.clone(),
460                    item_name: item_name.clone(),
461                    key_path: key_path.clone(),
462                    template: expanded_template,
463                    props: props.clone(),
464                }
465            }
466            super::IRNode::Conditional {
467                value,
468                branches,
469                fallback,
470            } => {
471                // Expand branch children
472                let expanded_branches: Vec<super::ConditionalBranch> = branches
473                    .iter()
474                    .map(|branch| super::ConditionalBranch {
475                        pattern: branch.pattern.clone(),
476                        children: branch
477                            .children
478                            .iter()
479                            .map(|child| self.expand_ir_node_with_context(child, context_path))
480                            .collect(),
481                    })
482                    .collect();
483
484                // Expand fallback children if present
485                let expanded_fallback = fallback.as_ref().map(|fb| {
486                    fb.iter()
487                        .map(|child| self.expand_ir_node_with_context(child, context_path))
488                        .collect()
489                });
490
491                super::IRNode::Conditional {
492                    value: value.clone(),
493                    branches: expanded_branches,
494                    fallback: expanded_fallback,
495                }
496            }
497        }
498    }
499
500    /// Replace Children() placeholders with actual children
501    /// Supports named slots via Children().slot("header")
502    fn replace_children_slots(
503        &self,
504        template_children: &im::Vector<Arc<Element>>,
505        actual_children: &im::Vector<Arc<Element>>,
506        _context_path: Option<&str>,
507    ) -> im::Vector<Arc<Element>> {
508        let mut result = im::Vector::new();
509
510        for child in template_children {
511            if child.element_type == "Children" {
512                // Check if this is a named slot via .slot() applicator
513                // Applicators are stored as props with format "applicatorName.argIndex"
514                let slot_name = self.get_slot_name(&child.props);
515
516                // If named slot, filter children by slot applicator
517                // Otherwise, include all children that don't have a slot applicator
518                if let Some(slot) = slot_name {
519                    for c in actual_children.iter() {
520                        if self.get_slot_name(&c.props) == Some(slot) {
521                            result.push_back(Arc::clone(c));
522                        }
523                    }
524                } else {
525                    // Default slot - children without slot applicator
526                    for c in actual_children.iter() {
527                        if self.get_slot_name(&c.props).is_none() {
528                            result.push_back(Arc::clone(c));
529                        }
530                    }
531                }
532            } else {
533                // Not a Children() placeholder - keep as is but recurse into its children
534                let mut new_child = (**child).clone();
535                new_child.children =
536                    self.replace_children_slots(&child.children, actual_children, _context_path);
537                result.push_back(Arc::new(new_child));
538            }
539        }
540
541        result
542    }
543
544    /// Extract slot name from applicators
545    /// Looks for .slot("name") which becomes prop "slot.0" = "name"
546    fn get_slot_name<'a>(&self, props: &'a super::Props) -> Option<&'a str> {
547        props.get("slot.0").and_then(|v| {
548            if let super::Value::Static(serde_json::Value::String(s)) = v {
549                Some(s.as_str())
550            } else {
551                None
552            }
553        })
554    }
555}
556
557impl Default for ComponentRegistry {
558    fn default() -> Self {
559        Self::new()
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use crate::ir::Value;
567
568    #[test]
569    fn test_dynamic_component_resolution() {
570        let mut registry = ComponentRegistry::new();
571
572        // Set up a resolver that returns component source and path
573        registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
574            if name == "Header" {
575                Some(ResolvedComponent {
576                    source: r#"Row { Text("Header") }"#.to_string(),
577                    path: "/components/Header.hypen".to_string(),
578                    passthrough: false,
579                    lazy: false,
580                })
581            } else {
582                None
583            }
584        }));
585
586        // Create an element that references the unregistered Header component
587        let element = Element::new("Column").with_child(Element::new("Header"));
588
589        // Expand should trigger resolution
590        let expanded = registry.expand(&element);
591
592        // Should have expanded Header into Row { Text }
593        assert_eq!(expanded.element_type, "Column");
594        assert_eq!(expanded.children.len(), 1);
595        let row = &expanded.children[0];
596        assert_eq!(row.element_type, "Row");
597        // Text child is in ir_children (from ast_to_ir_node path)
598        assert_eq!(row.ir_children.len(), 1);
599        match &row.ir_children[0] {
600            crate::ir::IRNode::Element(text) => assert_eq!(text.element_type, "Text"),
601            other => panic!("Expected Element, got {:?}", other),
602        }
603    }
604
605    #[test]
606    fn test_component_resolution_with_path_context() {
607        let mut registry = ComponentRegistry::new();
608
609        // Set up a resolver that resolves based on context path
610        registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
611            match (name, context) {
612                ("Button", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
613                    source: r#"Text("Home Button")"#.to_string(),
614                    path: "/components/buttons/HomeButton.hypen".to_string(),
615                    passthrough: false,
616                    lazy: false,
617                }),
618                ("Button", Some("/pages/About.hypen")) => Some(ResolvedComponent {
619                    source: r#"Text("About Button")"#.to_string(),
620                    path: "/components/buttons/AboutButton.hypen".to_string(),
621                    passthrough: false,
622                    lazy: false,
623                }),
624                _ => None,
625            }
626        }));
627
628        // Register a component with a source path
629        let home_element =
630            Element::new("Text").with_prop("0", Value::Static(serde_json::json!("Home")));
631        let home_component = Component::new("Home", move |_| home_element.clone())
632            .with_source_path("/pages/Home.hypen");
633        registry.register(home_component);
634
635        // Create an element that uses Button from Home context
636        let element = Element::new("Column")
637            .with_child(Element::new("Home").with_child(Element::new("Button")));
638
639        let expanded = registry.expand(&element);
640
641        // Button should resolve differently based on its context
642        assert_eq!(expanded.element_type, "Column");
643    }
644
645    #[test]
646    fn test_component_resolution_caching() {
647        let mut registry = ComponentRegistry::new();
648        let call_count = Arc::new(std::sync::Mutex::new(0));
649        let call_count_clone = call_count.clone();
650
651        // Resolver that tracks calls
652        registry.set_resolver(Arc::new(move |name: &str, _context: Option<&str>| {
653            if name == "Button" {
654                *call_count_clone.lock().unwrap() += 1;
655                Some(ResolvedComponent {
656                    source: r#"Text("Click")"#.to_string(),
657                    path: "/components/Button.hypen".to_string(),
658                    passthrough: false,
659                    lazy: false,
660                })
661            } else {
662                None
663            }
664        }));
665
666        // First expansion should call resolver
667        let element1 = Element::new("Button");
668        let _ = registry.expand(&element1);
669        assert_eq!(*call_count.lock().unwrap(), 1);
670
671        // Second expansion should use cache
672        let element2 = Element::new("Button");
673        let _ = registry.expand(&element2);
674        assert_eq!(*call_count.lock().unwrap(), 1); // Still 1, not 2
675    }
676
677    #[test]
678    fn test_failed_resolution_cached() {
679        let mut registry = ComponentRegistry::new();
680        let call_count = Arc::new(std::sync::Mutex::new(0));
681        let call_count_clone = call_count.clone();
682
683        // Resolver that returns None
684        registry.set_resolver(Arc::new(move |_name: &str, _context: Option<&str>| {
685            *call_count_clone.lock().unwrap() += 1;
686            None
687        }));
688
689        // First expansion should call resolver
690        let element1 = Element::new("Unknown");
691        let _ = registry.expand(&element1);
692        assert_eq!(*call_count.lock().unwrap(), 1);
693
694        // Second expansion should use cached failure
695        let element2 = Element::new("Unknown");
696        let _ = registry.expand(&element2);
697        assert_eq!(*call_count.lock().unwrap(), 1); // Cached
698    }
699
700    #[test]
701    fn test_passthrough_component_preserves_props() {
702        let mut registry = ComponentRegistry::new();
703
704        // Register Router and Route as passthrough components
705        registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
706            if name == "Router" || name == "Route" {
707                Some(ResolvedComponent {
708                    source: String::new(), // Empty template for passthrough
709                    path: name.to_string(),
710                    passthrough: true,
711                    lazy: false,
712                })
713            } else if name == "HomePage" {
714                // Regular component with a simple template
715                Some(ResolvedComponent {
716                    source: "Text(\"Home\")".to_string(),
717                    path: name.to_string(),
718                    passthrough: false,
719                    lazy: false,
720                })
721            } else {
722                None
723            }
724        }));
725
726        // Build a tree structure like:
727        // Router {
728        //   Route("/") { HomePage }
729        //   Route("/about") { HomePage }
730        // }
731        let mut router = Element::new("Router");
732
733        let mut route1 = Element::new("Route");
734        route1
735            .props
736            .insert("0".to_string(), Value::Static(serde_json::json!("/")));
737        route1
738            .children
739            .push_back(std::sync::Arc::new(Element::new("HomePage")));
740
741        let mut route2 = Element::new("Route");
742        route2
743            .props
744            .insert("0".to_string(), Value::Static(serde_json::json!("/about")));
745        route2
746            .children
747            .push_back(std::sync::Arc::new(Element::new("HomePage")));
748
749        router.children.push_back(std::sync::Arc::new(route1));
750        router.children.push_back(std::sync::Arc::new(route2));
751
752        // Expand the tree
753        let expanded = registry.expand(&router);
754
755        // Verify Router is preserved
756        assert_eq!(expanded.element_type, "Router");
757        assert_eq!(expanded.children.len(), 2);
758
759        // Verify first Route preserves its path prop
760        let expanded_route1 = &expanded.children[0];
761        assert_eq!(expanded_route1.element_type, "Route");
762        if let Some(Value::Static(path)) = expanded_route1.props.get("0") {
763            assert_eq!(path.as_str().unwrap(), "/");
764        } else {
765            panic!("Route 1 missing path prop");
766        }
767
768        // Verify second Route preserves its path prop
769        let expanded_route2 = &expanded.children[1];
770        assert_eq!(expanded_route2.element_type, "Route");
771        if let Some(Value::Static(path)) = expanded_route2.props.get("0") {
772            assert_eq!(path.as_str().unwrap(), "/about");
773        } else {
774            panic!("Route 2 missing path prop");
775        }
776
777        // Verify children are expanded (HomePage should be replaced with Text)
778        assert_eq!(expanded_route1.children.len(), 1);
779        assert_eq!(expanded_route1.children[0].element_type, "Text");
780    }
781}