use hypen_engine::{
ir::{ast_to_ir_node, IRNode, Value},
lifecycle::{Module, ModuleInstance},
reactive::{DependencyGraph, Scheduler},
reconcile::{reconcile_ir, InstanceTree, Patch},
};
use serde_json::json;
fn parse_to_element(input: &str) -> hypen_engine::Element {
let component = hypen_parser::parse_component(input).unwrap();
match ast_to_ir_node(&component) {
IRNode::Element(e) => e,
other => panic!("Expected Element, got {:?}", other),
}
}
#[test]
fn test_template_string_parsing() {
let source = r#"Text("Counter: @{state.counter}")"#;
let element = parse_to_element(source);
let prop = element.props.get("0").expect("Should have prop 0");
match prop {
Value::TemplateString { template, bindings } => {
assert_eq!(template, "Counter: @{state.counter}");
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].full_path(), "counter");
}
other => panic!("Expected TemplateString, got {:?}", other),
}
}
#[test]
fn test_template_string_dependency_registration() {
let source = r#"Text("Counter: @{state.counter}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"counter": 0});
let _patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
let node_id = tree.root().expect("Should have root");
let affected = dependencies.get_affected_nodes("counter");
assert!(
affected.contains(&node_id),
"Node should be registered as dependent on 'counter' path"
);
}
#[test]
fn test_template_string_initial_render() {
let source = r#"Text("Counter: @{state.counter}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"counter": 42});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
let _node_id = tree.root().expect("Should have root");
let create_patch = patches.iter().find(|p| matches!(p, Patch::Create { .. }));
assert!(create_patch.is_some(), "Should have a Create patch");
if let Some(Patch::Create { props, .. }) = create_patch {
let prop_value = props.get("0").expect("Should have prop 0");
assert_eq!(
prop_value.as_str().unwrap(),
"Counter: 42",
"Template string should be interpolated with state value"
);
}
}
#[test]
fn test_template_string_state_update() {
let source = r#"Text("Counter: @{state.counter}")"#;
let element = parse_to_element(source);
let module = Module::new("TestModule");
let initial_state = json!({"counter": 0});
let mut instance = ModuleInstance::new(module, initial_state);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let mut scheduler = Scheduler::new();
let _patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, instance.get_state(), &mut dependencies);
let node_id = tree.root().expect("Should have root");
tree.set_root(node_id);
let node = tree.get(node_id).unwrap();
assert_eq!(
node.props.get("0").and_then(|v| v.as_str()),
Some("Counter: 0"),
"Initial render should show Counter: 0"
);
instance.update_state(json!({"counter": 1}));
let affected = dependencies.get_affected_nodes("counter");
for &id in &affected {
scheduler.mark_dirty(id);
}
let update_patches =
hypen_engine::render::render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
assert!(!update_patches.is_empty(), "Should have update patches");
let set_prop_patch = update_patches
.iter()
.find(|p| matches!(p, Patch::SetProp { name, .. } if name == "0"));
assert!(
set_prop_patch.is_some(),
"Should have a SetProp patch for prop 0"
);
if let Some(Patch::SetProp { value, .. }) = set_prop_patch {
assert_eq!(
value.as_str().unwrap(),
"Counter: 1",
"SetProp should have interpolated value 'Counter: 1'"
);
}
}
#[test]
fn test_template_string_multiple_bindings() {
let source = r#"Text("@{state.greeting}, @{state.name}!")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"greeting": "Hello", "name": "World"});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
let node_id = tree.root().expect("Should have root");
assert!(
dependencies
.get_affected_nodes("greeting")
.contains(&node_id),
"Node should depend on 'greeting'"
);
assert!(
dependencies.get_affected_nodes("name").contains(&node_id),
"Node should depend on 'name'"
);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
let prop_value = props.get("0").expect("Should have prop 0");
assert_eq!(
prop_value.as_str().unwrap(),
"Hello, World!",
"Multiple bindings should all be interpolated"
);
}
}
#[test]
fn test_static_string_not_registered_as_dependency() {
let source = r#"Text("Hello World")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({});
let _ = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
let node_id = tree.root().expect("Should have root");
let all_affected = dependencies.get_affected_nodes("anything");
assert!(
!all_affected.contains(&node_id),
"Static string should not be registered as dependency"
);
}
#[test]
fn test_template_string_in_child_tree() {
let source = r#"Column { Text("Counter: @{state.counter}") }"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"counter": 0});
let _patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
let root_id = tree.root().expect("Should have root");
let root_node = tree.get(root_id).expect("Root should exist");
assert!(!root_node.children.is_empty(), "Column should have children");
let child_id = root_node.children[0];
let affected = dependencies.get_affected_nodes("counter");
assert!(
affected.contains(&child_id),
"Child node should be registered as dependent on 'counter'"
);
let module = Module::new("TestModule");
let mut instance = ModuleInstance::new(module, state);
instance.update_state(json!({"counter": 5}));
let mut scheduler = Scheduler::new();
for &id in &affected {
scheduler.mark_dirty(id);
}
let update_patches =
hypen_engine::render::render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
let set_prop = update_patches
.iter()
.find(|p| matches!(p, Patch::SetProp { name, .. } if name == "0"));
assert!(set_prop.is_some(), "Should have SetProp patch for child");
if let Some(Patch::SetProp { value, .. }) = set_prop {
assert_eq!(
value.as_str().unwrap(),
"Counter: 5",
"Child template string should be interpolated with new state"
);
}
}
#[test]
fn test_ternary_expression_evaluation() {
let source = r#"Text("@{state.active ? 'Active' : 'Inactive'}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"active": true});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
let prop_value = props.get("0").expect("Should have prop 0");
assert_eq!(
prop_value.as_str().unwrap(),
"Active",
"Ternary should evaluate to 'Active' when state.active is true"
);
} else {
panic!("No Create patch found");
}
}
#[test]
fn test_ternary_expression_false_condition() {
let source = r#"Text("@{state.active ? 'Active' : 'Inactive'}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"active": false});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
let prop_value = props.get("0").expect("Should have prop 0");
assert_eq!(
prop_value.as_str().unwrap(),
"Inactive",
"Ternary should evaluate to 'Inactive' when state.active is false"
);
}
}
#[test]
fn test_ternary_expression_with_colors() {
let source = r#"Column { }.backgroundColor("@{state.selected ? '#FFA7E1' : '#374151'}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"selected": true});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
let bg_color = props
.get("backgroundColor.0")
.expect("Should have backgroundColor.0");
assert_eq!(
bg_color.as_str().unwrap(),
"#FFA7E1",
"Color should be #FFA7E1 when selected is true"
);
}
}
#[test]
fn test_comparison_expression() {
let source = r#"Text("@{state.count > 10 ? 'Many' : 'Few'}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"count": 15});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
let prop_value = props.get("0").expect("Should have prop 0");
assert_eq!(
prop_value.as_str().unwrap(),
"Many",
"Should evaluate to 'Many' when count > 10"
);
}
}
#[test]
fn test_logical_and_expression() {
let source = r#"Text("@{state.a && state.b ? 'Both true' : 'Not both'}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"a": true, "b": true});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
let prop_value = props.get("0").expect("Should have prop 0");
assert_eq!(
prop_value.as_str().unwrap(),
"Both true",
"Should evaluate to 'Both true' when both a and b are true"
);
}
}
#[test]
fn test_mixed_expression_with_text() {
let source = r#"Text("Status: @{state.loading ? 'Loading...' : 'Ready'}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"loading": true});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
let prop_value = props.get("0").expect("Should have prop 0");
assert_eq!(
prop_value.as_str().unwrap(),
"Status: Loading...",
"Should combine static text with expression result"
);
}
}
#[test]
fn test_string_concatenation_expression() {
let source = r#"Text("@{state.first + ' ' + state.last}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({"first": "John", "last": "Doe"});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
let prop_value = props.get("0").expect("Should have prop 0");
assert_eq!(
prop_value.as_str().unwrap(),
"John Doe",
"Should concatenate strings"
);
}
}
#[test]
fn test_expression_state_update() {
let source = r#"Text("@{state.selected ? 'Selected' : 'Not selected'}")"#;
let element = parse_to_element(source);
let module = Module::new("TestModule");
let initial_state = json!({"selected": false});
let mut instance = ModuleInstance::new(module, initial_state);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let mut scheduler = Scheduler::new();
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, instance.get_state(), &mut dependencies);
let node_id = tree.root().expect("Should have root");
tree.set_root(node_id);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
assert_eq!(
props.get("0").unwrap().as_str().unwrap(),
"Not selected",
"Initial render should show 'Not selected'"
);
}
instance.update_state(json!({"selected": true}));
let affected = dependencies.get_affected_nodes("selected");
for &id in &affected {
scheduler.mark_dirty(id);
}
let update_patches =
hypen_engine::render::render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
let set_prop = update_patches
.iter()
.find(|p| matches!(p, Patch::SetProp { name, .. } if name == "0"));
assert!(
set_prop.is_some(),
"Should have SetProp patch after state change"
);
if let Some(Patch::SetProp { value, .. }) = set_prop {
assert_eq!(
value.as_str().unwrap(),
"Selected",
"Expression should re-evaluate to 'Selected' after state change"
);
}
}
#[test]
fn test_complex_nested_expression() {
let source =
r#"Text("@{state.user.premium && state.user.age >= 18 ? 'VIP Adult' : 'Standard'}")"#;
let element = parse_to_element(source);
let mut tree = InstanceTree::new();
let mut dependencies = DependencyGraph::new();
let state = json!({
"user": {
"premium": true,
"age": 25
}
});
let patches = reconcile_ir(&mut tree, &IRNode::Element(element.clone()), None, &state, &mut dependencies);
if let Some(Patch::Create { props, .. }) =
patches.iter().find(|p| matches!(p, Patch::Create { .. }))
{
let prop_value = props.get("0").expect("Should have prop 0");
assert_eq!(
prop_value.as_str().unwrap(),
"VIP Adult",
"Complex expression should evaluate correctly"
);
}
}