mod common;
use common::*;
use hypen_engine::ir::component::{Component, ComponentRegistry, ResolvedComponent};
use hypen_engine::ir::{Element, Value};
use serde_json::json;
use std::sync::Arc;
#[test]
fn test_expand_simple_component_no_children() {
let mut registry = ComponentRegistry::new();
let button_template = Element::new("Text").with_prop("text", Value::Static(json!("Click")));
let button = Component::new("Button", move |_props| button_template.clone());
registry.register(button);
let element = Element::new("Button");
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Text");
assert_prop_static_value(&expanded, "text", &json!("Click"));
}
#[test]
fn test_expand_component_with_arguments() {
let mut registry = ComponentRegistry::new();
let button = Component::new("Button", |props| {
let text = props
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("Default");
Element::new("Text").with_prop("text", Value::Static(json!(text)))
});
registry.register(button);
let element = Element::new("Button").with_prop("text", Value::Static(json!("Click Me")));
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Text");
assert_prop_static_value(&expanded, "text", &json!("Click Me"));
}
#[test]
fn test_expand_component_with_children() {
let mut registry = ComponentRegistry::new();
let card = Component::new("Card", |_props| {
let mut column = Element::new("Column");
column
.children
.push_back(std::sync::Arc::new(Element::new("Children"))); column
});
registry.register(card);
let card_element = Element::new("Card")
.with_child(text_element("Title"))
.with_child(text_element("Body"));
let expanded = registry.expand(&card_element);
assert_element_type(&expanded, "Column");
assert_eq!(expanded.children.len(), 2);
assert_element_type(&expanded.children[0], "Text");
assert_element_type(&expanded.children[1], "Text");
}
#[test]
fn test_expand_nested_components() {
let mut registry = ComponentRegistry::new();
let logo = Component::new("Logo", |_props| text_element("MyApp"));
registry.register(logo);
let header = Component::new("Header", |_props| {
Element::new("Row").with_child(Element::new("Logo"))
});
registry.register(header);
let page = Component::new("Page", |_props| {
Element::new("Column").with_child(Element::new("Header"))
});
registry.register(page);
let page_element = Element::new("Page");
let expanded = registry.expand(&page_element);
assert_element_type(&expanded, "Column");
assert_eq!(expanded.children.len(), 1);
assert_element_type(&expanded.children[0], "Row"); assert_eq!(expanded.children[0].children.len(), 1);
assert_element_type(&expanded.children[0].children[0], "Text"); }
#[test]
fn test_expand_with_context_path() {
let mut registry = ComponentRegistry::new();
let header = Component::new("Header", |_props| text_element("Header"))
.with_source_path("/components/Header.hypen");
registry.register(header);
let element = Element::new("Header");
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Text");
}
#[test]
fn test_expand_component_not_in_registry() {
let mut registry = ComponentRegistry::new();
let element = Element::new("UnknownComponent");
let expanded = registry.expand(&element);
assert_element_type(&expanded, "UnknownComponent");
}
#[test]
fn test_expand_component_triggers_resolver() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
if name == "DynamicButton" {
Some(ResolvedComponent {
source: r#"Text("Resolved")"#.to_string(),
path: "/components/DynamicButton.hypen".to_string(),
passthrough: false,
lazy: false,
})
} else {
None
}
}));
let element = Element::new("DynamicButton");
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Text");
assert_prop_static_value(&expanded, "0", &json!("Resolved"));
}
#[test]
fn test_expand_very_deep_nesting_10_levels() {
let mut registry = ComponentRegistry::new();
for i in 0..10 {
let next_level = if i < 9 {
format!("Level{}", i + 1)
} else {
"Text".to_string()
};
let level_name = format!("Level{}", i);
let component = Component::new(level_name.clone(), move |_props| {
Element::new(&next_level).with_prop("text", Value::Static(json!("Deep")))
});
registry.register(component);
}
let element = Element::new("Level0");
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Level1");
}
#[test]
fn test_lazy_component_flag_detected() {
let element = Element::new("LazyComponent").with_prop("__lazy", Value::Static(json!(true)));
let is_lazy = element
.props
.get("__lazy")
.and_then(|v| {
if let Value::Static(val) = v {
val.as_bool()
} else {
None
}
})
.unwrap_or(false);
assert!(is_lazy, "Element should be detected as lazy");
}
#[test]
fn test_lazy_component_children_not_expanded() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
if name == "LazyModule" {
Some(ResolvedComponent {
source: String::new(),
path: "/modules/LazyModule.hypen".to_string(),
passthrough: false,
lazy: true,
})
} else {
None
}
}));
let element = Element::new("LazyModule").with_child(Element::new("ExpensiveComponent"));
let expanded = registry.expand(&element);
assert_element_type(&expanded, "LazyModule");
assert_eq!(expanded.children.len(), 1);
assert_element_type(&expanded.children[0], "ExpensiveComponent");
let has_lazy_flag = expanded.props.get("__lazy").is_some();
assert!(has_lazy_flag, "Lazy component should have __lazy prop");
}
#[test]
fn test_lazy_component_props_preserved() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
if name == "LazyCard" {
Some(ResolvedComponent {
source: String::new(),
path: "/components/LazyCard.hypen".to_string(),
passthrough: false,
lazy: true,
})
} else {
None
}
}));
let element = Element::new("LazyCard")
.with_prop("title", Value::Static(json!("My Card")))
.with_prop("count", Value::Static(json!(42)));
let expanded = registry.expand(&element);
assert_prop_static_value(&expanded, "title", &json!("My Card"));
assert_prop_static_value(&expanded, "count", &json!(42));
assert!(expanded.props.get("__lazy").is_some());
}
#[test]
fn test_lazy_component_registration() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
if name == "ProfilePage" {
Some(ResolvedComponent {
source: "Column { Text(\"Profile\") }".to_string(),
path: "/pages/ProfilePage.hypen".to_string(),
passthrough: false,
lazy: true,
})
} else {
None
}
}));
let element = Element::new("ProfilePage");
let expanded = registry.expand(&element);
let component = registry.get("ProfilePage", None);
assert!(component.is_some());
assert!(component.unwrap().lazy);
assert!(expanded.props.get("__lazy").is_some());
}
#[test]
fn test_mixed_lazy_and_regular_children() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
if name == "LazyWidget" {
Some(ResolvedComponent {
source: String::new(),
path: "/widgets/LazyWidget.hypen".to_string(),
passthrough: false,
lazy: true,
})
} else {
None
}
}));
let element = column_with_children(vec![
Element::new("LazyWidget"),
text_element("Regular Text"),
]);
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Column");
assert_eq!(expanded.children.len(), 2);
assert_element_type(&expanded.children[0], "LazyWidget");
assert!(expanded.children[0].props.get("__lazy").is_some());
assert_element_type(&expanded.children[1], "Text");
assert!(expanded.children[1].props.get("__lazy").is_none());
}
#[test]
fn test_passthrough_component_preserves_element_type() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
if name == "Router" {
Some(ResolvedComponent {
source: String::new(),
path: "/runtime/Router.hypen".to_string(),
passthrough: true,
lazy: false,
})
} else {
None
}
}));
let element = Element::new("Router").with_prop("base", Value::Static(json!("/app")));
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Router");
assert_prop_static_value(&expanded, "base", &json!("/app"));
}
#[test]
fn test_passthrough_children_expanded() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
if name == "Router" {
Some(ResolvedComponent {
source: String::new(),
path: "/runtime/Router.hypen".to_string(),
passthrough: true,
lazy: false,
})
} else if name == "HomePage" {
Some(ResolvedComponent {
source: r#"Text("Home")"#.to_string(),
path: "/pages/HomePage.hypen".to_string(),
passthrough: false,
lazy: false,
})
} else {
None
}
}));
let element = Element::new("Router").with_child(Element::new("HomePage"));
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Router");
assert_eq!(expanded.children.len(), 1);
assert_element_type(&expanded.children[0], "Text");
}
#[test]
fn test_passthrough_context_propagation() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
match (name, context) {
("Container", _) => Some(ResolvedComponent {
source: String::new(),
path: "/layouts/Container.hypen".to_string(),
passthrough: true,
lazy: false,
}),
("Widget", Some("/layouts/Container.hypen")) => Some(ResolvedComponent {
source: r#"Text("Container Widget")"#.to_string(),
path: "/layouts/widgets/Widget.hypen".to_string(),
passthrough: false,
lazy: false,
}),
_ => None,
}
}));
let element = Element::new("Container").with_child(Element::new("Widget"));
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Container");
assert_eq!(expanded.children.len(), 1);
assert_element_type(&expanded.children[0], "Text");
assert_prop_static_value(&expanded.children[0], "0", &json!("Container Widget"));
}
#[test]
fn test_multiple_passthrough_layers() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
if name == "OuterContainer" || name == "InnerContainer" {
Some(ResolvedComponent {
source: String::new(),
path: format!("/{}.hypen", name),
passthrough: true,
lazy: false,
})
} else {
None
}
}));
let element = Element::new("OuterContainer")
.with_prop("outer", Value::Static(json!("A")))
.with_child(
Element::new("InnerContainer")
.with_prop("inner", Value::Static(json!("B")))
.with_child(text_element("Content")),
);
let expanded = registry.expand(&element);
assert_element_type(&expanded, "OuterContainer");
assert_prop_static_value(&expanded, "outer", &json!("A"));
assert_eq!(expanded.children.len(), 1);
assert_element_type(&expanded.children[0], "InnerContainer");
assert_prop_static_value(&expanded.children[0], "inner", &json!("B"));
assert_eq!(expanded.children[0].children.len(), 1);
assert_element_type(&expanded.children[0].children[0], "Text");
}
#[test]
fn test_passthrough_preserves_bindings() {
let mut registry = ComponentRegistry::new();
registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
if name == "Route" {
Some(ResolvedComponent {
source: String::new(),
path: "/runtime/Route.hypen".to_string(),
passthrough: true,
lazy: false,
})
} else {
None
}
}));
let element = Element::new("Route")
.with_prop("path", Value::Static(json!("/")))
.with_prop(
"active",
Value::Binding(hypen_engine::reactive::Binding::state(vec![
"isActive".to_string()
])),
);
let expanded = registry.expand(&element);
assert_prop_static_value(&expanded, "path", &json!("/"));
assert_binding_value(expanded.props.get("active").unwrap(), "isActive");
}
#[test]
fn test_children_slot_basic_replacement() {
let mut registry = ComponentRegistry::new();
let card = Component::new("Card", |_props| {
Element::new("Column").with_child(Element::new("Children"))
});
registry.register(card);
let element = Element::new("Card").with_child(text_element("Hello"));
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Column");
assert_eq!(expanded.children.len(), 1);
assert_element_type(&expanded.children[0], "Text");
}
#[test]
fn test_named_slot_with_slot_applicator() {
let mut registry = ComponentRegistry::new();
let layout = Component::new("Layout", |_props| {
let mut header_slot = Element::new("Children");
header_slot
.props
.insert("slot.0".to_string(), Value::Static(json!("header")));
Element::new("Column").with_child(header_slot)
});
registry.register(layout);
let mut header_text = text_element("Header");
header_text
.props
.insert("slot.0".to_string(), Value::Static(json!("header")));
let element = Element::new("Layout").with_child(header_text);
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Column");
assert_eq!(expanded.children.len(), 1);
assert_element_type(&expanded.children[0], "Text");
}
#[test]
fn test_default_slot_without_applicator() {
let mut registry = ComponentRegistry::new();
let wrapper = Component::new("Wrapper", |_props| {
Element::new("Column").with_child(Element::new("Children"))
});
registry.register(wrapper);
let element = Element::new("Wrapper").with_child(text_element("Default Content"));
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Column");
assert_eq!(expanded.children.len(), 1);
assert_element_type(&expanded.children[0], "Text");
}
#[test]
fn test_multiple_named_slots() {
let mut registry = ComponentRegistry::new();
let page = Component::new("Page", |_props| {
let mut header_slot = Element::new("Children");
header_slot
.props
.insert("slot.0".to_string(), Value::Static(json!("header")));
let mut footer_slot = Element::new("Children");
footer_slot
.props
.insert("slot.0".to_string(), Value::Static(json!("footer")));
Element::new("Column")
.with_child(header_slot)
.with_child(footer_slot)
});
registry.register(page);
let mut header_child = text_element("Header");
header_child
.props
.insert("slot.0".to_string(), Value::Static(json!("header")));
let mut footer_child = text_element("Footer");
footer_child
.props
.insert("slot.0".to_string(), Value::Static(json!("footer")));
let element = Element::new("Page")
.with_child(header_child)
.with_child(footer_child);
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Column");
assert_eq!(expanded.children.len(), 2);
assert_element_type(&expanded.children[0], "Text");
assert_element_type(&expanded.children[1], "Text");
}
#[test]
fn test_empty_slot_removed() {
let mut registry = ComponentRegistry::new();
let empty_wrapper = Component::new("EmptyWrapper", |_props| {
Element::new("Column").with_child(Element::new("Children"))
});
registry.register(empty_wrapper);
let element = Element::new("EmptyWrapper");
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Column");
assert_eq!(expanded.children.len(), 0);
}
#[test]
fn test_multiple_children_same_slot() {
let mut registry = ComponentRegistry::new();
let card = Component::new("Card", |_props| {
let mut body_slot = Element::new("Children");
body_slot
.props
.insert("slot.0".to_string(), Value::Static(json!("body")));
Element::new("Column").with_child(body_slot)
});
registry.register(card);
let mut text_child = text_element("Description");
text_child
.props
.insert("slot.0".to_string(), Value::Static(json!("body")));
let mut image_child = image_element("photo.jpg");
image_child
.props
.insert("slot.0".to_string(), Value::Static(json!("body")));
let element = Element::new("Card")
.with_child(text_child)
.with_child(image_child);
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Column");
assert_eq!(expanded.children.len(), 2);
assert_element_type(&expanded.children[0], "Text");
assert_element_type(&expanded.children[1], "Image");
}
#[test]
fn test_nested_slot_replacement() {
let mut registry = ComponentRegistry::new();
let nested_layout = Component::new("NestedLayout", |_props| {
Element::new("Row").with_child(Element::new("Column").with_child(Element::new("Children")))
});
registry.register(nested_layout);
let element = Element::new("NestedLayout").with_child(text_element("Nested Content"));
let expanded = registry.expand(&element);
assert_element_type(&expanded, "Row");
assert_eq!(expanded.children.len(), 1);
assert_element_type(&expanded.children[0], "Column");
assert_eq!(expanded.children[0].children.len(), 1);
assert_element_type(&expanded.children[0].children[0], "Text");
}
#[test]
fn test_slot_name_extraction() {
let mut element = text_element("Content");
element
.props
.insert("slot.0".to_string(), Value::Static(json!("customName")));
let slot_name = element.props.get("slot.0").and_then(|v| {
if let Value::Static(json_val) = v {
json_val.as_str()
} else {
None
}
});
assert_eq!(slot_name, Some("customName"));
}
#[test]
fn test_component_cached_after_first_resolution() {
let mut registry = ComponentRegistry::new();
let call_count = Arc::new(std::sync::Mutex::new(0));
let call_count_clone = call_count.clone();
registry.set_resolver(Arc::new(move |name: &str, _context: Option<&str>| {
if name == "CachedButton" {
*call_count_clone.lock().unwrap() += 1;
Some(ResolvedComponent {
source: r#"Text("Cached")"#.to_string(),
path: "/components/CachedButton.hypen".to_string(),
passthrough: false,
lazy: false,
})
} else {
None
}
}));
let element1 = Element::new("CachedButton");
let _ = registry.expand(&element1);
let element2 = Element::new("CachedButton");
let _ = registry.expand(&element2);
assert_eq!(*call_count.lock().unwrap(), 1);
}
#[test]
fn test_primitive_registration_skips_resolution() {
let mut registry = ComponentRegistry::new();
let call_count = Arc::new(std::sync::Mutex::new(0));
let call_count_clone = call_count.clone();
registry.set_resolver(Arc::new(move |_name: &str, _context: Option<&str>| {
*call_count_clone.lock().unwrap() += 1;
None
}));
registry.register_primitive("Text");
let element = text_element("Hello");
let _ = registry.expand(&element);
assert_eq!(*call_count.lock().unwrap(), 0);
}