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(None, 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(None, 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(None, 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(None, 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(None, 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(None, json!({"count": 1}));
engine.update_state(None, json!({"count": 2}));
engine.update_state(None, 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(None, 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);
}
#[test]
fn test_set_module_primary_action_scope_is_none() {
let mut engine = Engine::new();
let module_meta = Module::new("Counter")
.with_actions(vec!["increment".to_string(), "decrement".to_string()]);
let module = ModuleInstance::new(module_meta, json!({"count": 0}));
engine.set_module(module);
assert_eq!(engine.action_scope_for("increment"), None);
assert_eq!(engine.action_scope_for("decrement"), None);
assert_eq!(engine.action_scope_for("nonexistent"), None);
}
#[test]
fn test_register_module_named_action_scope() {
let mut engine = Engine::new();
let module_meta =
Module::new("Search").with_actions(vec!["submit".to_string(), "clear".to_string()]);
let module = ModuleInstance::new(module_meta, json!({}));
engine.register_module("search", module);
assert_eq!(engine.action_scope_for("submit"), Some("search".to_string()));
assert_eq!(engine.action_scope_for("clear"), Some("search".to_string()));
}
#[test]
fn test_set_module_twice_evicts_previous_primary_actions() {
let mut engine = Engine::new();
let first = ModuleInstance::new(
Module::new("First").with_actions(vec!["increment".to_string()]),
json!({}),
);
engine.set_module(first);
assert_eq!(engine.action_scope_for("increment"), None);
let second = ModuleInstance::new(
Module::new("Second").with_actions(vec!["reset".to_string()]),
json!({}),
);
engine.set_module(second);
let named = ModuleInstance::new(
Module::new("Other").with_actions(vec!["increment".to_string()]),
json!({}),
);
engine.register_module("other", named);
assert_eq!(
engine.action_scope_for("increment"),
Some("other".to_string())
);
}
#[test]
fn test_register_module_twice_evicts_previous_named_actions() {
let mut engine = Engine::new();
let first = ModuleInstance::new(
Module::new("Search").with_actions(vec!["submit".to_string(), "clear".to_string()]),
json!({}),
);
engine.register_module("search", first);
assert_eq!(engine.action_scope_for("submit"), Some("search".to_string()));
assert_eq!(engine.action_scope_for("clear"), Some("search".to_string()));
let second = ModuleInstance::new(
Module::new("Search").with_actions(vec!["query".to_string()]),
json!({}),
);
engine.register_module("search", second);
assert_eq!(engine.action_scope_for("submit"), None);
assert_eq!(engine.action_scope_for("clear"), None);
assert_eq!(engine.action_scope_for("query"), Some("search".to_string()));
}
#[test]
fn test_set_and_register_module_coexist() {
let mut engine = Engine::new();
engine.set_module(ModuleInstance::new(
Module::new("Primary").with_actions(vec!["a".to_string()]),
json!({}),
));
engine.register_module(
"search",
ModuleInstance::new(
Module::new("Search").with_actions(vec!["b".to_string()]),
json!({}),
),
);
assert_eq!(engine.action_scope_for("a"), None); assert_eq!(engine.action_scope_for("b"), Some("search".to_string()));
engine.set_module(ModuleInstance::new(
Module::new("Primary2").with_actions(vec!["c".to_string()]),
json!({}),
));
assert_eq!(engine.action_scope_for("a"), None); assert_eq!(engine.action_scope_for("b"), Some("search".to_string())); assert_eq!(engine.action_scope_for("c"), None); }
#[test]
fn test_set_context_invalidates_deep_data_source_bindings() {
let mut engine = Engine::new();
let module = ModuleInstance::new(Module::new("Page"), json!({}));
engine.set_module(module);
engine.set_context("spacetime", json!({"user": {"name": "Alice", "age": 30}}));
let source = r#"Text("@{spacetime.user.name}")"#;
let component = hypen_parser::parse_component(source).expect("parse");
let ir_node = hypen_engine::ir::ast_to_ir_node(&component);
engine.render_ir_node(&ir_node);
let (patches, callback) = patch_capture();
engine.set_render_callback(callback);
engine.set_context("spacetime", json!({"user": {"name": "Bob", "age": 31}}));
let captured = patches.lock().unwrap();
let saw_bob = captured.iter().any(|p| match p {
hypen_engine::reconcile::Patch::SetProp { value, .. } => {
value == &json!("Bob")
}
hypen_engine::reconcile::Patch::SetText { text, .. } => text == "Bob",
hypen_engine::reconcile::Patch::Create { props, .. } => props
.values()
.any(|v| v == &json!("Bob")),
_ => false,
});
assert!(
saw_bob,
"Expected a patch carrying 'Bob' after set_context replaced the deep provider state; got: {:?}",
*captured
);
}