mod common;
use common::*;
use hypen_engine::dispatch::Action;
use hypen_engine::ir::{Component, Element};
use hypen_engine::lifecycle::{Module, ModuleInstance};
use hypen_engine::Engine;
use serde_json::json;
use std::sync::{Arc, Mutex};
#[test]
fn test_engine_new_creates_empty_state() {
let engine = Engine::new();
assert_eq!(engine.revision(), 0);
}
#[test]
fn test_engine_default_is_identical_to_new() {
let engine1 = Engine::new();
let engine2 = Engine::default();
assert_eq!(engine1.revision(), engine2.revision());
assert_eq!(engine1.revision(), 0);
}
#[test]
fn test_engine_revision_starts_at_zero() {
let engine = Engine::new();
assert_eq!(engine.revision(), 0);
}
#[test]
fn test_register_component_adds_to_registry() {
let mut engine = Engine::new();
let component = Component::new("Button", |_props| text_element("Click me"));
engine.register_component(component);
let element = Element::new("Button");
let expanded = engine.component_registry_mut().expand(&element);
assert_element_type(&expanded, "Text");
}
#[test]
fn test_register_component_replaces_existing() {
let mut engine = Engine::new();
let component1 = Component::new("Button", |_props| text_element("First"));
let component2 = Component::new("Button", |_props| text_element("Second"));
engine.register_component(component1);
engine.register_component(component2);
let element = Element::new("Button");
let expanded = engine.component_registry_mut().expand(&element);
assert_element_type(&expanded, "Text");
}
#[test]
fn test_component_resolver_fallback_when_not_in_registry() {
let mut engine = Engine::new();
let resolved = Arc::new(Mutex::new(false));
let resolved_clone = resolved.clone();
engine.set_component_resolver(move |name, _context| {
*resolved_clone.lock().unwrap() = true;
if name == "DynamicButton" {
Some(hypen_engine::ir::ResolvedComponent {
source: "Text(\"Dynamic\")".to_string(),
path: "DynamicButton.hypen".to_string(),
passthrough: false,
lazy: false,
})
} else {
None
}
});
let element = Element::new("DynamicButton");
let _ = engine.component_registry_mut().expand(&element);
assert!(*resolved.lock().unwrap());
}
#[test]
fn test_component_resolver_returns_none_falls_back_gracefully() {
let mut engine = Engine::new();
engine.set_component_resolver(|_name, _context| None);
let element = Element::new("UnknownComponent");
let expanded = engine.component_registry_mut().expand(&element);
assert_element_type(&expanded, "UnknownComponent");
}
#[test]
fn test_multiple_component_registrations() {
let mut engine = Engine::new();
for i in 0..10 {
let name = format!("Component{}", i);
let content = format!("Content {}", i);
let component = Component::new(name.clone(), move |_props| text_element(&content));
engine.register_component(component);
}
for i in 0..10 {
let element = Element::new(format!("Component{}", i));
let expanded = engine.component_registry_mut().expand(&element);
assert_element_type(&expanded, "Text");
}
}
#[test]
fn test_render_simple_text_element() {
let mut engine = Engine::new();
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
let element = text_element("Hello");
engine.render(&element);
let captured = patches.lock().unwrap();
assert_has_create(&captured);
assert!(captured.len() >= 1);
}
#[test]
fn test_render_nested_column_with_children() {
let mut engine = Engine::new();
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
let element = column_with_children(vec![text_element("First"), text_element("Second")]);
engine.render(&element);
let captured = patches.lock().unwrap();
assert_eq!(count_creates(&captured), 3);
}
#[test]
fn test_render_increments_revision() {
let mut engine = Engine::new();
assert_eq!(engine.revision(), 0);
let element = text_element("Hello");
engine.render(&element);
assert_eq!(engine.revision(), 1);
}
#[test]
fn test_render_callback_receives_patches() {
let mut engine = Engine::new();
let invoked = Arc::new(Mutex::new(false));
let invoked_clone = invoked.clone();
engine.set_render_callback(move |patches| {
*invoked_clone.lock().unwrap() = !patches.is_empty();
});
let element = text_element("Hello");
engine.render(&element);
assert!(*invoked.lock().unwrap());
}
#[test]
fn test_render_same_tree_twice_minimal_patches() {
let mut engine = Engine::new();
let element = text_element("Hello");
engine.render(&element);
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
engine.render(&element);
let captured = patches.lock().unwrap();
assert_no_changes(&captured);
}
#[test]
fn test_render_with_state_bindings() {
let mut engine = Engine::new();
let module_meta = Module::new("TestModule");
let module = ModuleInstance::new(module_meta, json!({"name": "Alice"}));
engine.set_module(module);
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
let element = text_element_with_binding("name");
engine.render(&element);
let captured = patches.lock().unwrap();
let has_alice = captured.iter().any(|p| {
if let hypen_engine::reconcile::Patch::Create { props, .. } = p {
props
.get("text")
.map(|v| v == &json!("Alice"))
.unwrap_or(false)
} else {
false
}
});
assert!(has_alice, "Expected Create patch with text='Alice'");
}
#[test]
fn test_render_empty_element_tree() {
let mut engine = Engine::new();
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
let element = column_with_children(vec![]);
engine.render(&element);
let captured = patches.lock().unwrap();
assert!(captured.len() >= 1);
}
#[test]
fn test_render_multiple_times_increments_revision() {
let mut engine = Engine::new();
let element = text_element("Test");
for _ in 0..5 {
engine.render(&element);
}
assert_eq!(engine.revision(), 5);
}
#[test]
fn test_update_state_increments_revision() {
let mut engine = Engine::new();
let module_meta = Module::new("TestModule");
let module = ModuleInstance::new(module_meta, json!({"count": 0}));
engine.set_module(module);
let element = text_element_with_binding("count");
engine.render(&element);
assert_eq!(engine.revision(), 1);
engine.update_state(json!({"count": 1}));
assert_eq!(engine.revision(), 2);
}
#[test]
fn test_update_state_with_nested_path() {
let mut engine = Engine::new();
let module_meta = Module::new("TestModule");
let module = ModuleInstance::new(module_meta, json!({"user": {"name": "Alice"}}));
engine.set_module(module);
let element = text_element_with_binding("user.name");
engine.render(&element);
assert_eq!(engine.revision(), 1);
engine.update_state(json!({"user": {"name": "Bob"}}));
assert_eq!(engine.revision(), 2);
}
#[test]
fn test_update_state_with_no_dependents() {
let mut engine = Engine::new();
let element = text_element("Static");
engine.render(&element);
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
engine.update_state(json!({"foo": "bar"}));
let captured = patches.lock().unwrap();
assert_eq!(captured.len(), 0);
}
#[test]
fn test_update_state_with_multiple_bindings() {
let mut engine = Engine::new();
let module_meta = Module::new("TestModule");
let module = ModuleInstance::new(module_meta, json!({"a": "1", "b": "2"}));
engine.set_module(module);
let element = column_with_children(vec![
text_element_with_binding("a"),
text_element_with_binding("b"),
]);
engine.render(&element);
assert_eq!(engine.revision(), 1);
engine.update_state(json!({"a": "10", "b": "20"}));
assert_eq!(engine.revision(), 2);
}
#[test]
fn test_update_state_with_empty_patch() {
let mut engine = Engine::new();
let element = text_element("Test");
engine.render(&element);
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
engine.update_state(json!({}));
let captured = patches.lock().unwrap();
assert_eq!(captured.len(), 0);
}
#[test]
fn test_multiple_state_updates_batch_correctly() {
let mut engine = Engine::new();
let module_meta = Module::new("TestModule");
let module = ModuleInstance::new(module_meta, json!({"count": 0}));
engine.set_module(module);
let element = text_element_with_binding("count");
engine.render(&element);
engine.update_state(json!({"count": 1}));
engine.update_state(json!({"count": 2}));
engine.update_state(json!({"count": 3}));
assert_eq!(engine.revision(), 4); }
#[test]
fn test_dispatch_action_calls_registered_handler() {
let mut engine = Engine::new();
let called = Arc::new(Mutex::new(false));
let called_clone = called.clone();
engine.on_action("signIn", move |_action| {
*called_clone.lock().unwrap() = true;
});
let action = Action::new("signIn");
let result = engine.dispatch_action(action);
assert!(result.is_ok());
assert!(*called.lock().unwrap());
}
#[test]
fn test_dispatch_action_with_payload() {
let mut engine = Engine::new();
let received = Arc::new(Mutex::new(None));
let received_clone = received.clone();
engine.on_action("submit", move |action| {
*received_clone.lock().unwrap() = action.payload.clone();
});
let action = Action::new("submit").with_payload(json!({"value": 42}));
let _ = engine.dispatch_action(action);
let payload = received.lock().unwrap();
assert!(payload.is_some());
assert_eq!(payload.as_ref().unwrap()["value"], 42);
}
#[test]
fn test_dispatch_action_without_handler_returns_error() {
let mut engine = Engine::new();
let action = Action::new("unknown");
let result = engine.dispatch_action(action);
assert!(result.is_err());
}
#[test]
fn test_dispatch_multiple_actions_sequentially() {
let mut engine = Engine::new();
let count = Arc::new(Mutex::new(0));
for i in 1..=3 {
let count_clone = count.clone();
engine.on_action(format!("action{}", i), move |_| {
*count_clone.lock().unwrap() += 1;
});
}
for i in 1..=3 {
let action = Action::new(format!("action{}", i));
let _ = engine.dispatch_action(action);
}
assert_eq!(*count.lock().unwrap(), 3);
}
#[test]
fn test_action_handler_can_be_registered_after_render() {
let mut engine = Engine::new();
let element = text_element("Button");
engine.render(&element);
let called = Arc::new(Mutex::new(false));
let called_clone = called.clone();
engine.on_action("click", move |_| {
*called_clone.lock().unwrap() = true;
});
let action = Action::new("click");
let _ = engine.dispatch_action(action);
assert!(*called.lock().unwrap());
}
#[test]
fn test_set_module_registers_instance() {
let mut engine = Engine::new();
let module_meta = Module::new("ProfilePage");
let module = ModuleInstance::new(module_meta, json!({}));
engine.set_module(module);
let element = text_element("Test");
engine.render(&element);
}
#[test]
fn test_render_with_module_state() {
let mut engine = Engine::new();
let module = user_module(); engine.set_module(module);
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
let element = text_element_with_binding("user.name");
engine.render(&element);
let captured = patches.lock().unwrap();
let has_alice = captured.iter().any(|p| {
if let hypen_engine::reconcile::Patch::Create { props, .. } = p {
props
.get("text")
.map(|v| v == &json!("Alice"))
.unwrap_or(false)
} else {
false
}
});
assert!(has_alice, "Expected Create patch with user.name='Alice'");
}
#[test]
fn test_module_state_update_increments_revision() {
let mut engine = Engine::new();
let module_meta = Module::new("CounterModule");
let module = ModuleInstance::new(module_meta, json!({"count": 0}));
engine.set_module(module);
let element = text_element_with_binding("count");
engine.render(&element);
assert_eq!(engine.revision(), 1);
engine.update_state(json!({"count": 5}));
assert_eq!(engine.revision(), 2);
}
#[test]
fn test_multiple_module_instances_replace_each_other() {
let mut engine = Engine::new();
let module1 = ModuleInstance::new(Module::new("Module1"), json!({"value": "first"}));
engine.set_module(module1);
let module2 = ModuleInstance::new(Module::new("Module2"), json!({"value": "second"}));
engine.set_module(module2);
let element = text_element_with_binding("value");
engine.render(&element);
}
#[test]
fn test_render_very_deep_tree() {
let mut engine = Engine::new();
let element = deep_tree(50);
engine.render(&element);
assert_eq!(engine.revision(), 1);
}
#[test]
fn test_render_tree_with_many_children() {
let mut engine = Engine::new();
let element = wide_tree(100);
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
engine.render(&element);
let captured = patches.lock().unwrap();
assert_eq!(count_creates(&captured), 101);
}