use hypen_engine::ir::{ast_to_ir_node, ConditionalBranch, Element, IRNode, Props, Value};
use hypen_engine::reactive::{Binding, DependencyGraph};
use hypen_engine::reconcile::{reconcile_ir, InstanceTree, Patch};
use hypen_parser::parse_component;
use serde_json::json;
#[test]
fn test_foreach_patches_exclude_container() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"items": [
{"id": "1", "name": "Item 1"},
{"id": "2", "name": "Item 2"}
]
});
let ir_node = IRNode::ForEach {
source: Binding::state(vec!["items".to_string()]),
item_name: "item".to_string(),
key_path: Some("id".to_string()),
template: vec![IRNode::Element(Element::new("Text"))],
props: Props::new(),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
for patch in &patches {
if let Patch::Create { element_type, .. } = patch {
assert_ne!(
element_type, "__ForEach",
"Patches should not contain __ForEach"
);
}
}
let create_patches: Vec<_> = patches
.iter()
.filter(|p| matches!(p, Patch::Create { .. }))
.collect();
assert_eq!(
create_patches.len(),
2,
"Should have 2 Create patches for Text elements"
);
}
#[test]
fn test_conditional_patches_exclude_container() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "status": "loading" });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(json!("loading")),
vec![IRNode::Element(Element::new("Spinner"))],
)],
fallback: Some(vec![IRNode::Element(Element::new("Content"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
for patch in &patches {
if let Patch::Create { element_type, .. } = patch {
assert_ne!(
element_type, "__Conditional",
"Patches should not contain __Conditional"
);
}
}
let create_patches: Vec<_> = patches
.iter()
.filter(|p| matches!(p, Patch::Create { .. }))
.collect();
assert_eq!(
create_patches.len(),
1,
"Should have 1 Create patch for Spinner"
);
}
#[test]
fn test_foreach_basic_parsing() {
let input = r#"
ForEach(items: @state.todos, as: "todo", key: "id") {
Text(@item.name)
}
"#;
let component = parse_component(input).unwrap();
let ir_node = ast_to_ir_node(&component);
match ir_node {
IRNode::ForEach {
source,
item_name,
key_path,
template,
..
} => {
assert!(source.is_state(), "Source should be a state binding");
assert_eq!(source.path, vec!["todos"]);
assert_eq!(item_name, "todo");
assert_eq!(key_path, Some("id".to_string()));
assert_eq!(template.len(), 1);
}
_ => panic!("Expected ForEach IRNode, got {:?}", ir_node),
}
}
#[test]
fn test_foreach_positional_syntax() {
let input = r#"
ForEach(@state.items) {
Text(@item.name)
}
"#;
let component = parse_component(input).unwrap();
let ir_node = ast_to_ir_node(&component);
match ir_node {
IRNode::ForEach {
source,
item_name,
key_path,
..
} => {
assert!(source.is_state(), "Source should be a state binding");
assert_eq!(source.path, vec!["items"]);
assert_eq!(item_name, "item"); assert_eq!(key_path, None);
}
_ => panic!("Expected ForEach IRNode, got {:?}", ir_node),
}
}
#[test]
fn test_foreach_creates_container() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"items": [
{"id": "1", "name": "Item 1"},
{"id": "2", "name": "Item 2"}
]
});
let ir_node = IRNode::ForEach {
source: Binding::state(vec!["items".to_string()]),
item_name: "item".to_string(),
key_path: Some("id".to_string()),
template: vec![IRNode::Element(Element::new("Text").with_prop(
"0",
Value::Binding(Binding::item(vec!["name".to_string()])),
))],
props: Props::new(),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert_eq!(container.element_type, "__ForEach");
assert_eq!(container.children.len(), 2);
let affected = deps.get_affected_nodes("items");
assert!(affected.contains(&node_id));
}
#[test]
fn test_foreach_empty_array() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"items": []
});
let ir_node = IRNode::ForEach {
source: Binding::state(vec!["items".to_string()]),
item_name: "item".to_string(),
key_path: None,
template: vec![IRNode::Element(Element::new("Text"))],
props: Props::new(),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 0);
}
#[test]
fn test_foreach_nested() {
let input = r#"
ForEach(items: @state.categories, as: "category", key: "id") {
Column {
Text(@item.name)
ForEach(items: @item.products, as: "product") {
Text(@item.title)
}
}
}
"#;
let component = parse_component(input).unwrap();
let ir_node = ast_to_ir_node(&component);
match ir_node {
IRNode::ForEach { template, .. } => {
assert_eq!(template.len(), 1);
match &template[0] {
IRNode::Element(element) => {
assert_eq!(element.element_type, "Column");
assert_eq!(element.ir_children.len(), 2);
}
_ => panic!("Expected Element in template"),
}
}
_ => panic!("Expected ForEach IRNode"),
}
}
#[test]
fn test_when_basic_parsing() {
let input = r#"
When(value: @state.status) {
Case(match: "loading") {
Text("Loading...")
}
Case(match: "ready") {
Text("Ready!")
}
Else {
Text("Unknown")
}
}
"#;
let component = parse_component(input).unwrap();
let ir_node = ast_to_ir_node(&component);
match ir_node {
IRNode::Conditional {
value,
branches,
fallback,
module_scope: None,
} => {
match value {
Value::Binding(b) => {
assert!(b.is_state());
assert_eq!(b.path, vec!["status"]);
}
_ => panic!("Expected binding value"),
}
assert_eq!(branches.len(), 2);
assert!(fallback.is_some());
}
_ => panic!("Expected Conditional IRNode, got {:?}", ir_node),
}
}
#[test]
fn test_when_renders_matching_branch() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"status": "loading"
});
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!("loading")),
vec![IRNode::Element(Element::new("Spinner"))],
),
ConditionalBranch::new(
Value::Static(json!("ready")),
vec![IRNode::Element(Element::new("Content"))],
),
],
fallback: Some(vec![IRNode::Element(Element::new("Error"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert_eq!(container.element_type, "__Conditional");
assert_eq!(container.children.len(), 1);
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Spinner");
}
#[test]
fn test_when_renders_fallback() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"status": "unknown_status"
});
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(json!("loading")),
vec![IRNode::Element(Element::new("Spinner"))],
)],
fallback: Some(vec![IRNode::Element(Element::new("Fallback"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 1);
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Fallback");
}
#[test]
fn test_when_no_match_no_fallback() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"status": "unknown"
});
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(json!("loading")),
vec![IRNode::Element(Element::new("Spinner"))],
)],
fallback: None,
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 0);
}
#[test]
fn test_if_basic_parsing() {
let input = r#"
If(condition: @state.isLoggedIn) {
Text("Welcome!")
Else {
Text("Please log in")
}
}
"#;
let component = parse_component(input).unwrap();
let ir_node = ast_to_ir_node(&component);
match ir_node {
IRNode::Conditional {
value,
branches,
fallback,
module_scope: None,
} => {
match value {
Value::Binding(b) => {
assert!(b.is_state());
assert_eq!(b.path, vec!["isLoggedIn"]);
}
_ => panic!("Expected binding value"),
}
assert_eq!(branches.len(), 1);
match &branches[0].pattern {
Value::Static(v) => assert_eq!(*v, json!(true)),
_ => panic!("Expected static true pattern"),
}
assert!(fallback.is_some());
}
_ => panic!("Expected Conditional IRNode, got {:?}", ir_node),
}
}
#[test]
fn test_if_true_condition() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"isVisible": true
});
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["isVisible".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(json!(true)),
vec![IRNode::Element(Element::new("Content"))],
)],
fallback: Some(vec![IRNode::Element(Element::new("Hidden"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Content");
}
#[test]
fn test_if_false_condition() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"isVisible": false
});
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["isVisible".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(json!(true)),
vec![IRNode::Element(Element::new("Content"))],
)],
fallback: Some(vec![IRNode::Element(Element::new("Hidden"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Hidden");
}
#[test]
fn test_list_creates_wrapper_with_foreach_child() {
let input = r#"
List(@state.todos) {
Text(@item.name)
}
"#;
let component = parse_component(input).unwrap();
let ir_node = ast_to_ir_node(&component);
match &ir_node {
IRNode::Element(element) => {
assert_eq!(element.element_type, "List", "Wrapper should be a List element");
assert_eq!(element.ir_children.len(), 1, "Should have one ForEach IR child");
match &element.ir_children[0] {
IRNode::ForEach {
source, item_name, ..
} => {
assert!(source.is_state());
assert_eq!(source.path, vec!["todos"]);
assert_eq!(item_name, "item");
}
_ => panic!("Expected ForEach IR child, got {:?}", element.ir_children[0]),
}
}
_ => panic!(
"Expected Element(List) with ForEach child, got {:?}",
ir_node
),
}
}
#[test]
fn test_foreach_reconciliation_add_items() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let initial_state = json!({
"items": [
{"id": "1", "name": "Item 1"},
{"id": "2", "name": "Item 2"}
]
});
let ir_node = IRNode::ForEach {
source: Binding::state(vec!["items".to_string()]),
item_name: "item".to_string(),
key_path: Some("id".to_string()),
template: vec![IRNode::Element(Element::new("Text"))],
props: Props::new(),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &initial_state, &mut deps));
let node_id = tree.root().expect("Should have root");
let new_state = json!({
"items": [
{"id": "1", "name": "Item 1"},
{"id": "2", "name": "Item 2"},
{"id": "3", "name": "Item 3"}
]
});
let mut update_patches = Vec::new();
update_patches.extend(reconcile_ir(&mut tree, &ir_node, None, &new_state, &mut deps));
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 3);
}
#[test]
fn test_conditional_reconciliation_branch_change() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let initial_state = json!({ "status": "loading" });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!("loading")),
vec![IRNode::Element(Element::new("Spinner"))],
),
ConditionalBranch::new(
Value::Static(json!("ready")),
vec![IRNode::Element(Element::new("Content"))],
),
],
fallback: None,
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &initial_state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let initial_child = tree.get(container.children[0]).unwrap();
assert_eq!(initial_child.element_type, "Spinner");
let new_state = json!({ "status": "ready" });
let mut update_patches = Vec::new();
update_patches.extend(reconcile_ir(&mut tree, &ir_node, None, &new_state, &mut deps));
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 1);
let new_child = tree.get(container.children[0]).unwrap();
assert_eq!(new_child.element_type, "Content");
}
#[test]
fn test_complex_nested_control_flow() {
let input = r#"
Column {
If(condition: @state.isLoading) {
Text("Loading...")
Else {
ForEach(items: @state.users, as: "user", key: "id") {
Row {
Text(@item.name)
When(value: @item.role) {
Case(match: "admin") {
Text("Admin Badge")
}
Case(match: "user") {
Text("User Badge")
}
}
}
}
}
}
}
"#;
let component = parse_component(input).unwrap();
let ir_node = ast_to_ir_node(&component);
match ir_node {
IRNode::Element(element) => {
assert_eq!(element.element_type, "Column");
assert_eq!(element.ir_children.len(), 1);
}
_ => panic!("Expected Element for Column, got {:?}", ir_node),
}
}
#[test]
fn test_foreach_with_props() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"items": [{"id": "1", "name": "Item 1"}]
});
let mut props = Props::new();
props.insert("padding.0".to_string(), Value::Static(json!(16)));
props.insert("gap.0".to_string(), Value::Static(json!(8)));
let ir_node = IRNode::ForEach {
source: Binding::state(vec!["items".to_string()]),
item_name: "item".to_string(),
key_path: None,
template: vec![IRNode::Element(Element::new("Text"))],
props,
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert!(container.props.contains_key("padding.0"));
assert!(container.props.contains_key("gap.0"));
assert_eq!(container.props.get("padding.0"), Some(&json!(16)));
assert_eq!(container.props.get("gap.0"), Some(&json!(8)));
}
#[test]
fn test_when_expression_greater_than() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "count": 150 });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["count".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::TemplateString {
template: "@{value > 100}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("Large"))],
),
ConditionalBranch::new(
Value::TemplateString {
template: "@{value > 10}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("Medium"))],
),
],
fallback: Some(vec![IRNode::Element(Element::new("Small"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 1);
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Large");
}
#[test]
fn test_when_expression_medium_range() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "count": 50 });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["count".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::TemplateString {
template: "@{value > 100}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("Large"))],
),
ConditionalBranch::new(
Value::TemplateString {
template: "@{value > 10}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("Medium"))],
),
],
fallback: Some(vec![IRNode::Element(Element::new("Small"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Medium");
}
#[test]
fn test_when_expression_fallback() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "count": 5 });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["count".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::TemplateString {
template: "@{value > 100}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("Large"))],
),
ConditionalBranch::new(
Value::TemplateString {
template: "@{value > 10}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("Medium"))],
),
],
fallback: Some(vec![IRNode::Element(Element::new("Small"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Small");
}
#[test]
fn test_when_expression_string_equality() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "role": "admin" });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["role".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::TemplateString {
template: "@{value == 'admin'}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("AdminPanel"))],
),
ConditionalBranch::new(
Value::TemplateString {
template: "@{value == 'user'}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("UserDashboard"))],
),
],
fallback: Some(vec![IRNode::Element(Element::new("GuestView"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "AdminPanel");
}
#[test]
fn test_when_expression_logical_and() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({
"user": {
"isAdmin": true,
"isActive": true
}
});
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["user".to_string()])),
branches: vec![ConditionalBranch::new(
Value::TemplateString {
template: "@{value.isAdmin && value.isActive}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("ActiveAdmin"))],
)],
fallback: Some(vec![IRNode::Element(Element::new("Regular"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "ActiveAdmin");
}
#[test]
fn test_when_mixed_static_and_expression() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "status": "loading" });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!("loading")),
vec![IRNode::Element(Element::new("Spinner"))],
),
ConditionalBranch::new(
Value::TemplateString {
template: "@{value == 'error'}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("ErrorView"))],
),
],
fallback: Some(vec![IRNode::Element(Element::new("Content"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Spinner");
}
#[test]
fn test_when_wildcard_underscore() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "value": "any_string_here" });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["value".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!("specific")),
vec![IRNode::Element(Element::new("Specific"))],
),
ConditionalBranch::new(
Value::Static(json!("_")),
vec![IRNode::Element(Element::new("Wildcard"))],
),
],
fallback: None,
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Wildcard");
}
#[test]
fn test_when_wildcard_asterisk() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "count": 42 });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["count".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!(100)),
vec![IRNode::Element(Element::new("Hundred"))],
),
ConditionalBranch::new(
Value::Static(json!("*")),
vec![IRNode::Element(Element::new("AnyValue"))],
),
],
fallback: None,
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "AnyValue");
}
#[test]
fn test_when_wildcard_matches_null() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "value": null });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["value".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(json!("_")),
vec![IRNode::Element(Element::new("CatchAll"))],
)],
fallback: None,
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "CatchAll");
}
#[test]
fn test_when_multiple_values_matches_first() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "status": "loading" });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!(["loading", "pending", "fetching"])),
vec![IRNode::Element(Element::new("InProgress"))],
),
ConditionalBranch::new(
Value::Static(json!(["success", "complete"])),
vec![IRNode::Element(Element::new("Done"))],
),
],
fallback: Some(vec![IRNode::Element(Element::new("Other"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "InProgress");
}
#[test]
fn test_when_multiple_values_matches_middle() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "status": "pending" });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(json!(["loading", "pending", "fetching"])),
vec![IRNode::Element(Element::new("InProgress"))],
)],
fallback: Some(vec![IRNode::Element(Element::new("Other"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "InProgress");
}
#[test]
fn test_when_multiple_values_no_match() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "status": "error" });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!(["loading", "pending"])),
vec![IRNode::Element(Element::new("InProgress"))],
),
ConditionalBranch::new(
Value::Static(json!(["success", "complete"])),
vec![IRNode::Element(Element::new("Done"))],
),
],
fallback: Some(vec![IRNode::Element(Element::new("ErrorView"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "ErrorView");
}
#[test]
fn test_when_multiple_numeric_values() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "code": 404 });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["code".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!([200, 201, 204])),
vec![IRNode::Element(Element::new("Success"))],
),
ConditionalBranch::new(
Value::Static(json!([400, 401, 403, 404])),
vec![IRNode::Element(Element::new("ClientError"))],
),
ConditionalBranch::new(
Value::Static(json!([500, 502, 503])),
vec![IRNode::Element(Element::new("ServerError"))],
),
],
fallback: Some(vec![IRNode::Element(Element::new("Unknown"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "ClientError");
}
#[test]
fn test_when_exact_number_match() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "count": 42 });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["count".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(json!(42)),
vec![IRNode::Element(Element::new("TheAnswer"))],
)],
fallback: Some(vec![IRNode::Element(Element::new("Other"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "TheAnswer");
}
#[test]
fn test_when_exact_null_match() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "user": null });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["user".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(serde_json::Value::Null),
vec![IRNode::Element(Element::new("NoUser"))],
)],
fallback: Some(vec![IRNode::Element(Element::new("HasUser"))]),
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "NoUser");
}
#[test]
fn test_when_exact_boolean_match() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "isEnabled": true });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["isEnabled".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!(true)),
vec![IRNode::Element(Element::new("Enabled"))],
),
ConditionalBranch::new(
Value::Static(json!(false)),
vec![IRNode::Element(Element::new("Disabled"))],
),
],
fallback: None,
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Enabled");
}
#[test]
fn test_when_combined_patterns() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let state = json!({ "score": 85 });
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["score".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!(100)),
vec![IRNode::Element(Element::new("Perfect"))],
),
ConditionalBranch::new(
Value::Static(json!("90..99")),
vec![IRNode::Element(Element::new("GradeA"))],
),
ConditionalBranch::new(
Value::TemplateString {
template: "@{value >= 70}".to_string(),
bindings: vec![],
},
vec![IRNode::Element(Element::new("Passing"))],
),
ConditionalBranch::new(
Value::Static(json!("_")),
vec![IRNode::Element(Element::new("Failing"))],
),
],
fallback: None,
module_scope: None,
};
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
let child = tree.get(container.children[0]).unwrap();
assert_eq!(child.element_type, "Passing");
}
#[test]
fn test_conditional_reconciliation_grow_children() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["mode".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!("simple")),
vec![IRNode::Element(Element::new("Header"))],
),
ConditionalBranch::new(
Value::Static(json!("detailed")),
vec![
IRNode::Element(Element::new("Header")),
IRNode::Element(Element::new("Details")),
],
),
],
fallback: None,
module_scope: None,
};
let state = json!({ "mode": "simple" });
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 1);
let header_id = container.children[0];
assert_eq!(tree.get(header_id).unwrap().element_type, "Header");
let new_state = json!({ "mode": "detailed" });
let mut update_patches = Vec::new();
update_patches.extend(reconcile_ir(&mut tree, &ir_node, None, &new_state, &mut deps));
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 2, "Should now have 2 children");
let first_child = tree.get(container.children[0]).unwrap();
assert_eq!(first_child.element_type, "Header");
let second_child = tree.get(container.children[1]).unwrap();
assert_eq!(second_child.element_type, "Details");
let remove_patches: Vec<_> = update_patches
.iter()
.filter(|p| matches!(p, Patch::Remove { .. }))
.collect();
assert_eq!(
remove_patches.len(),
0,
"No nodes should be removed when growing"
);
let create_patches: Vec<_> = update_patches
.iter()
.filter(|p| matches!(p, Patch::Create { .. }))
.collect();
assert_eq!(
create_patches.len(),
1,
"Only the new Details child should be created"
);
}
#[test]
fn test_conditional_reconciliation_shrink_children() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["mode".to_string()])),
branches: vec![
ConditionalBranch::new(
Value::Static(json!("detailed")),
vec![
IRNode::Element(Element::new("Header")),
IRNode::Element(Element::new("Details")),
],
),
ConditionalBranch::new(
Value::Static(json!("simple")),
vec![IRNode::Element(Element::new("Header"))],
),
],
fallback: None,
module_scope: None,
};
let state = json!({ "mode": "detailed" });
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 2);
let new_state = json!({ "mode": "simple" });
let mut update_patches = Vec::new();
update_patches.extend(reconcile_ir(&mut tree, &ir_node, None, &new_state, &mut deps));
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 1, "Should now have 1 child");
assert_eq!(
tree.get(container.children[0]).unwrap().element_type,
"Header"
);
let remove_patches: Vec<_> = update_patches
.iter()
.filter(|p| matches!(p, Patch::Remove { .. }))
.collect();
assert!(
!remove_patches.is_empty(),
"Details child should be removed"
);
}
#[test]
fn test_conditional_reconciliation_to_no_match() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["status".to_string()])),
branches: vec![ConditionalBranch::new(
Value::Static(json!("active")),
vec![
IRNode::Element(Element::new("Header")),
IRNode::Element(Element::new("Content")),
],
)],
fallback: None,
module_scope: None,
};
let state = json!({ "status": "active" });
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
assert_eq!(tree.get(node_id).unwrap().children.len(), 2);
let new_state = json!({ "status": "unknown" });
let mut update_patches = Vec::new();
update_patches.extend(reconcile_ir(&mut tree, &ir_node, None, &new_state, &mut deps));
assert_eq!(
tree.get(node_id).unwrap().children.len(),
0,
"All children should be removed"
);
}
#[test]
fn test_conditional_reconciliation_tab_switch_reuses_structure() {
let mut tree = InstanceTree::new();
let mut patches = Vec::new();
let mut deps = DependencyGraph::new();
let make_tab = |label: &str| -> Vec<IRNode> {
let inner = Element::new("Column").with_child(Element::new(label));
vec![IRNode::Element(inner)]
};
let ir_node = IRNode::Conditional {
value: Value::Binding(Binding::state(vec!["tab".to_string()])),
branches: vec![
ConditionalBranch::new(Value::Static(json!("home")), make_tab("HomeContent")),
ConditionalBranch::new(
Value::Static(json!("settings")),
make_tab("SettingsContent"),
),
ConditionalBranch::new(Value::Static(json!("profile")), make_tab("ProfileContent")),
],
fallback: None,
module_scope: None,
};
let state = json!({ "tab": "home" });
patches.extend(reconcile_ir(&mut tree, &ir_node, None, &state, &mut deps));
let node_id = tree.root().expect("Should have root");
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 1);
let column_id = container.children[0];
assert_eq!(tree.get(column_id).unwrap().element_type, "Column");
let new_state = json!({ "tab": "settings" });
let mut update_patches = Vec::new();
update_patches.extend(reconcile_ir(&mut tree, &ir_node, None, &new_state, &mut deps));
let container = tree.get(node_id).unwrap();
assert_eq!(container.children.len(), 1);
let column_after = tree.get(container.children[0]).unwrap();
assert_eq!(column_after.element_type, "Column");
let column_id_str = format!("{:?}", column_id);
let remove_patches: Vec<_> = update_patches
.iter()
.filter(|p| {
if let Patch::Remove { id } = p {
*id == column_id_str
} else {
false
}
})
.collect();
assert_eq!(
remove_patches.len(),
0,
"Column should be reused via reconciliation, not removed"
);
}