mod common;
use common::*;
use hypen_engine::ir::{ast_to_ir_node, Component, Element};
use hypen_engine::lifecycle::{Module, ModuleInstance};
use hypen_engine::reconcile::Patch;
use hypen_engine::Engine;
use hypen_parser::{parse_document, ImportClause, ImportSource, ImportStatement};
use serde_json::json;
use std::sync::{Arc, Mutex};
#[test]
fn test_import_buffer_serialization_single_local() {
let imports = [ImportStatement::new(
ImportClause::Named(vec!["Button".to_string(), "Card".to_string()]),
ImportSource::Local("./components/ui".to_string()),
)];
let import_infos: Vec<serde_json::Value> = imports
.iter()
.map(|imp| {
let (source_path, source_type) = match &imp.source {
ImportSource::Local(p) => (p.as_str(), "local"),
ImportSource::Url(u) => (u.as_str(), "url"),
};
json!({
"names": imp.imported_names(),
"source_path": source_path,
"source_type": source_type,
})
})
.collect();
let json_bytes = serde_json::to_vec(&import_infos).unwrap();
let json_str = String::from_utf8(json_bytes).unwrap();
let parsed: Vec<serde_json::Value> = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0]["names"], json!(["Button", "Card"]));
assert_eq!(parsed[0]["source_path"], json!("./components/ui"));
assert_eq!(parsed[0]["source_type"], json!("local"));
}
#[test]
fn test_import_buffer_serialization_url() {
let imports = [ImportStatement::new(
ImportClause::Default("Widget".to_string()),
ImportSource::Url("https://cdn.example.com/widgets".to_string()),
)];
let import_infos: Vec<serde_json::Value> = imports
.iter()
.map(|imp| {
let (source_path, source_type) = match &imp.source {
ImportSource::Local(p) => (p.as_str(), "local"),
ImportSource::Url(u) => (u.as_str(), "url"),
};
json!({
"names": imp.imported_names(),
"source_path": source_path,
"source_type": source_type,
})
})
.collect();
let json_str = serde_json::to_string(&import_infos).unwrap();
let parsed: Vec<serde_json::Value> = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed[0]["names"], json!(["Widget"]));
assert_eq!(
parsed[0]["source_path"],
json!("https://cdn.example.com/widgets")
);
assert_eq!(parsed[0]["source_type"], json!("url"));
}
#[test]
fn test_import_buffer_serialization_multiple_mixed() {
let imports = [
ImportStatement::new(
ImportClause::Named(vec!["Button".to_string()]),
ImportSource::Local("./ui".to_string()),
),
ImportStatement::new(
ImportClause::Default("Dashboard".to_string()),
ImportSource::Url("https://cdn.example.com/dashboard".to_string()),
),
ImportStatement::new(
ImportClause::Named(vec!["A".to_string(), "B".to_string(), "C".to_string()]),
ImportSource::Local("../shared/components".to_string()),
),
];
let import_infos: Vec<serde_json::Value> = imports
.iter()
.map(|imp| {
let (source_path, source_type) = match &imp.source {
ImportSource::Local(p) => (p.as_str(), "local"),
ImportSource::Url(u) => (u.as_str(), "url"),
};
json!({
"names": imp.imported_names(),
"source_path": source_path,
"source_type": source_type,
})
})
.collect();
let json_str = serde_json::to_string(&import_infos).unwrap();
let parsed: Vec<serde_json::Value> = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed.len(), 3);
assert_eq!(parsed[0]["source_type"], json!("local"));
assert_eq!(parsed[1]["source_type"], json!("url"));
assert_eq!(parsed[2]["names"], json!(["A", "B", "C"]));
}
#[test]
fn test_import_buffer_serialization_empty() {
let imports: Vec<ImportStatement> = vec![];
let import_infos: Vec<serde_json::Value> = imports
.iter()
.map(|imp| {
let (source_path, source_type) = match &imp.source {
ImportSource::Local(p) => (p.as_str(), "local"),
ImportSource::Url(u) => (u.as_str(), "url"),
};
json!({
"names": imp.imported_names(),
"source_path": source_path,
"source_type": source_type,
})
})
.collect();
let json_str = serde_json::to_string(&import_infos).unwrap();
assert_eq!(json_str, "[]");
}
#[test]
fn test_render_document_with_pre_registered_import() {
let mut engine = Engine::new();
let patches = Arc::new(Mutex::new(Vec::new()));
let patches_clone = patches.clone();
engine.set_render_callback(move |p: &[Patch]| {
patches_clone.lock().unwrap().extend(p.iter().cloned());
});
let badge_component = Component::new("Badge", |_props| text_element("badge"));
engine.register_component(badge_component);
let input = r#"
import { Badge } from "./ui"
Column {
Text("Hello")
Badge()
}
"#;
let doc = parse_document(input).unwrap();
let ir_node = ast_to_ir_node(&doc.components[0]);
engine.render_ir_node(&ir_node);
let patches = patches.lock().unwrap();
let create_types: Vec<&str> = patches
.iter()
.filter_map(|p| match p {
Patch::Create { element_type, .. } => Some(element_type.as_str()),
_ => None,
})
.collect();
assert!(
create_types.contains(&"Column"),
"Expected Column in creates: {:?}",
create_types
);
assert!(
create_types.contains(&"Text"),
"Expected Text in creates: {:?}",
create_types
);
}
#[test]
fn test_document_imports_are_accessible_after_parsing() {
let input = r#"
import { Button, Card } from "./components/ui"
import Header from "./layout/header"
import { Widget } from "https://cdn.example.com/widgets"
Column {
Header()
Button(text: "Click")
Card()
Widget()
}
"#;
let doc = parse_document(input).unwrap();
assert_eq!(doc.imports.len(), 3);
assert_eq!(doc.imports[0].imported_names(), vec!["Button", "Card"]);
assert_eq!(doc.imports[0].source_path(), "./components/ui");
assert_eq!(doc.imports[1].imported_names(), vec!["Header"]);
assert_eq!(doc.imports[1].source_path(), "./layout/header");
assert_eq!(doc.imports[2].imported_names(), vec!["Widget"]);
assert_eq!(
doc.imports[2].source_path(),
"https://cdn.example.com/widgets"
);
}
#[test]
fn test_document_without_imports_has_empty_imports_vec() {
let input = r#"
Column {
Text("No imports here")
}
"#;
let doc = parse_document(input).unwrap();
assert!(doc.imports.is_empty());
assert_eq!(doc.components.len(), 1);
assert_eq!(doc.components[0].name, "Column");
}
#[test]
fn test_nested_import_chain_a_imports_b() {
let mut engine = Engine::new();
let patches = Arc::new(Mutex::new(Vec::new()));
let patches_clone = patches.clone();
engine.set_render_callback(move |p: &[Patch]| {
patches_clone.lock().unwrap().extend(p.iter().cloned());
});
let logo = Component::new("Logo", |_props| Element::new("Image"));
engine.register_component(logo);
let header = Component::new("Header", |_props| {
let mut row = Element::new("Row");
row.ir_children
.push(hypen_engine::ir::IRNode::Element(Element::new("Logo")));
row.ir_children
.push(hypen_engine::ir::IRNode::Element(text_element("App Title")));
row
});
engine.register_component(header);
let app_input = r#"
import { Header } from "./layout"
Column {
Header()
Text("Content")
}
"#;
let doc = parse_document(app_input).unwrap();
let ir_node = ast_to_ir_node(&doc.components[0]);
engine.render_ir_node(&ir_node);
let patches = patches.lock().unwrap();
let create_types: Vec<&str> = patches
.iter()
.filter_map(|p| match p {
Patch::Create { element_type, .. } => Some(element_type.as_str()),
_ => None,
})
.collect();
assert!(
create_types.contains(&"Column"),
"Missing Column: {:?}",
create_types
);
assert!(
create_types.contains(&"Row"),
"Missing Row (from Header): {:?}",
create_types
);
}
#[test]
fn test_circular_import_detection_via_visited_set() {
use std::collections::HashSet;
let mut visited = HashSet::new();
assert!(visited.insert("./components/a".to_string()));
assert!(visited.insert("./components/b".to_string()));
assert!(
!visited.insert("./components/a".to_string()),
"Should detect circular import"
);
}
#[test]
fn test_import_visited_set_uses_name_and_path() {
use std::collections::HashSet;
let mut visited = HashSet::new();
let key1 = format!("{}:{}", "./ui", "Button");
assert!(visited.insert(key1));
let key2 = format!("{}:{}", "./ui", "Card");
assert!(visited.insert(key2));
let key3 = format!("{}:{}", "./other", "Button");
assert!(visited.insert(key3));
let key4 = format!("{}:{}", "./ui", "Button");
assert!(!visited.insert(key4), "Should detect duplicate import key");
}
#[test]
fn test_three_level_component_chain() {
let mut engine = Engine::new();
let patches = Arc::new(Mutex::new(Vec::new()));
let patches_clone = patches.clone();
engine.set_render_callback(move |p: &[Patch]| {
patches_clone.lock().unwrap().extend(p.iter().cloned());
});
let widget = Component::new("Widget", |_props| text_element("widget"));
engine.register_component(widget);
let section = Component::new("Section", |_props| {
let mut col = Element::new("Column");
col.ir_children
.push(hypen_engine::ir::IRNode::Element(Element::new("Widget")));
col.ir_children
.push(hypen_engine::ir::IRNode::Element(text_element("section")));
col
});
engine.register_component(section);
let page = Component::new("Page", |_props| {
let mut col = Element::new("Column");
col.ir_children
.push(hypen_engine::ir::IRNode::Element(Element::new("Section")));
col.ir_children
.push(hypen_engine::ir::IRNode::Element(text_element("page")));
col
});
engine.register_component(page);
let app_input = r#"
import { Page } from "./pages"
Column {
Page()
Text("footer")
}
"#;
let doc = parse_document(app_input).unwrap();
let ir_node = ast_to_ir_node(&doc.components[0]);
engine.render_ir_node(&ir_node);
let patches = patches.lock().unwrap();
assert!(
patches.len() >= 5,
"Expected many patches for 4-level component tree, got {}",
patches.len()
);
}
#[test]
fn test_state_binding_in_document() {
let mut engine = Engine::new();
let patches = Arc::new(Mutex::new(Vec::new()));
let patches_clone = patches.clone();
engine.set_render_callback(move |p: &[Patch]| {
patches_clone.lock().unwrap().extend(p.iter().cloned());
});
let module_meta = Module::new("App");
let module = ModuleInstance::new(module_meta, json!({"title": "My App"}));
engine.set_module(module);
let input = r#"
import { AppHeader } from "./header"
Column {
Text("@{state.title}")
}
"#;
let doc = parse_document(input).unwrap();
engine.render_ir_node(&ast_to_ir_node(&doc.components[0]));
let patches = patches.lock().unwrap();
let has_title = patches.iter().any(|p| {
if let Patch::Create { props, .. } = p {
props
.get("0")
.map(|v| v == &json!("My App"))
.unwrap_or(false)
} else {
false
}
});
assert!(has_title, "Expected 'My App' in initial render");
}
#[test]
fn test_state_update_propagates_through_document() {
let mut engine = Engine::new();
let patches = Arc::new(Mutex::new(Vec::new()));
let patches_clone = patches.clone();
engine.set_render_callback(move |p: &[Patch]| {
patches_clone.lock().unwrap().extend(p.iter().cloned());
});
let module_meta = Module::new("App");
let module = ModuleInstance::new(module_meta, json!({"count": 0}));
engine.set_module(module);
let input = r#"
import { Counter } from "./counter"
Column {
Text("@{state.count}")
}
"#;
let doc = parse_document(input).unwrap();
engine.render_ir_node(&ast_to_ir_node(&doc.components[0]));
patches.lock().unwrap().clear();
engine.update_state(None, json!({"count": 99}));
let patches = patches.lock().unwrap();
let has_update = patches.iter().any(|p| match p {
Patch::SetProp { name, value, .. } => name == "0" && value == &json!(99),
_ => false,
});
assert!(
has_update,
"Expected SetProp with count=99. Got: {:?}",
*patches
);
}
#[test]
fn test_document_full_lifecycle() {
let mut engine = Engine::new();
let all_patches = Arc::new(Mutex::new(Vec::new()));
let patches_clone = all_patches.clone();
engine.set_render_callback(move |p: &[Patch]| {
patches_clone.lock().unwrap().extend(p.iter().cloned());
});
let module_meta = Module::new("Chat");
let module = ModuleInstance::new(module_meta, json!({"message": "Hello"}));
engine.set_module(module);
let input = r#"
import { MessageView } from "./chat"
Column {
Text("@{state.message}")
}
"#;
let doc = parse_document(input).unwrap();
engine.render_ir_node(&ast_to_ir_node(&doc.components[0]));
{
let patches = all_patches.lock().unwrap();
let has_hello = patches.iter().any(|p| {
if let Patch::Create { props, .. } = p {
props
.get("0")
.map(|v| v == &json!("Hello"))
.unwrap_or(false)
} else {
false
}
});
assert!(has_hello, "Expected 'Hello' in initial render");
}
all_patches.lock().unwrap().clear();
engine.update_state(None, json!({"message": "World"}));
{
let patches = all_patches.lock().unwrap();
let has_world = patches.iter().any(|p| match p {
Patch::SetProp { name, value, .. } => name == "0" && value == &json!("World"),
_ => false,
});
assert!(
has_world,
"Expected 'World' after state update. Got: {:?}",
*patches
);
}
}
#[test]
fn test_document_with_router_and_imports() {
let input = r#"
import { HomePage } from "./pages/home"
import { AboutPage } from "./pages/about"
Router {
Route(path: "/") {
HomePage()
}
Route(path: "/about") {
AboutPage()
}
}
"#;
let doc = parse_document(input).unwrap();
assert_eq!(doc.imports.len(), 2);
assert_eq!(doc.imports[0].imported_names(), vec!["HomePage"]);
assert_eq!(doc.imports[1].imported_names(), vec!["AboutPage"]);
assert_eq!(doc.components.len(), 1);
assert_eq!(doc.components[0].name, "Router");
}
#[test]
fn test_document_with_nested_layouts_and_imports() {
let input = r#"
import { Header } from "./layout/header"
import { Footer } from "./layout/footer"
import { Sidebar } from "./layout/sidebar"
import { MainContent } from "./pages/main"
Column {
Header()
Row {
Sidebar()
MainContent()
}
Footer()
}
"#;
let doc = parse_document(input).unwrap();
assert_eq!(doc.imports.len(), 4);
assert_eq!(doc.imports[0].source_path(), "./layout/header");
assert_eq!(doc.imports[1].source_path(), "./layout/footer");
assert_eq!(doc.imports[2].source_path(), "./layout/sidebar");
assert_eq!(doc.imports[3].source_path(), "./pages/main");
assert_eq!(doc.components.len(), 1);
assert_eq!(doc.components[0].name, "Column");
}