use hypen_engine::ir::NodeId;
use hypen_engine::reconcile::Patch;
use indexmap::indexmap;
use serde_json::json;
use std::sync::Arc;
fn test_node_id() -> NodeId {
NodeId::default()
}
#[test]
fn test_create_patch_basic() {
let node_id = test_node_id();
let props = indexmap! {
"text".to_string() => json!("Hello"),
"color".to_string() => json!("blue"),
};
let patch = Patch::create(node_id, "Text".to_string(), Arc::new(props.clone()));
match patch {
Patch::Create {
id,
element_type,
props: patch_props,
} => {
assert!(!id.is_empty());
assert_eq!(element_type, "Text");
assert_eq!(patch_props.get("text"), Some(&json!("Hello")));
assert_eq!(patch_props.get("color"), Some(&json!("blue")));
}
_ => panic!("Expected Create patch"),
}
}
#[test]
fn test_set_prop_patch() {
let node_id = test_node_id();
let patch = Patch::set_prop(node_id, "fontSize".to_string(), json!(18));
match patch {
Patch::SetProp { id, name, value } => {
assert!(!id.is_empty());
assert_eq!(name, "fontSize");
assert_eq!(value, json!(18));
}
_ => panic!("Expected SetProp patch"),
}
}
#[test]
fn test_set_text_patch() {
let node_id = test_node_id();
let patch = Patch::set_text(node_id, "Updated text".to_string());
match patch {
Patch::SetText { id, text } => {
assert!(!id.is_empty());
assert_eq!(text, "Updated text");
}
_ => panic!("Expected SetText patch"),
}
}
#[test]
fn test_insert_patch_without_before() {
let parent_id = test_node_id();
let child_id = test_node_id();
let patch = Patch::insert(parent_id, child_id, None);
match patch {
Patch::Insert {
parent_id: parent,
id,
before_id,
} => {
assert!(!parent.is_empty());
assert!(!id.is_empty());
assert_eq!(before_id, None);
}
_ => panic!("Expected Insert patch"),
}
}
#[test]
fn test_insert_patch_with_before() {
let parent_id = test_node_id();
let child_id = test_node_id();
let sibling_id = test_node_id();
let patch = Patch::insert(parent_id, child_id, Some(sibling_id));
match patch {
Patch::Insert {
parent_id: parent,
id,
before_id,
} => {
assert!(!parent.is_empty());
assert!(!id.is_empty());
assert!(before_id.is_some());
}
_ => panic!("Expected Insert patch"),
}
}
#[test]
fn test_insert_root_patch() {
let node_id = test_node_id();
let patch = Patch::insert_root(node_id);
match patch {
Patch::Insert {
parent_id,
id,
before_id,
} => {
assert_eq!(parent_id, "root");
assert!(!id.is_empty());
assert_eq!(before_id, None);
}
_ => panic!("Expected Insert patch"),
}
}
#[test]
fn test_move_patch() {
let parent_id = test_node_id();
let node_id = test_node_id();
let before_id = test_node_id();
let patch = Patch::move_node(parent_id, node_id, Some(before_id));
match patch {
Patch::Move {
parent_id: parent,
id,
before_id: before,
} => {
assert!(!parent.is_empty());
assert!(!id.is_empty());
assert!(before.is_some());
}
_ => panic!("Expected Move patch"),
}
}
#[test]
fn test_remove_patch() {
let node_id = test_node_id();
let patch = Patch::remove(node_id);
match patch {
Patch::Remove { id } => {
assert!(!id.is_empty());
}
_ => panic!("Expected Remove patch"),
}
}
#[test]
fn test_patch_clone() {
let node_id = test_node_id();
let original = Patch::set_text(node_id, "Original".to_string());
let cloned = original.clone();
match (&original, &cloned) {
(Patch::SetText { text: t1, .. }, Patch::SetText { text: t2, .. }) => {
assert_eq!(t1, t2);
}
_ => panic!("Expected SetText patches"),
}
}
#[test]
fn test_create_patch_with_empty_props() {
let node_id = test_node_id();
let props = indexmap! {};
let patch = Patch::create(node_id, "EmptyElement".to_string(), Arc::new(props));
match patch {
Patch::Create {
element_type,
props,
..
} => {
assert_eq!(element_type, "EmptyElement");
assert_eq!(props.len(), 0);
}
_ => panic!("Expected Create patch"),
}
}
#[test]
fn test_serialize_create_patch() {
let node_id = test_node_id();
let props = indexmap! {
"text".to_string() => json!("Hello"),
};
let patch = Patch::create(node_id, "Text".to_string(), Arc::new(props));
let json = serde_json::to_value(&patch).unwrap();
assert_eq!(json["type"], "create");
assert!(json.get("id").is_some());
assert!(json.get("elementType").is_some() || json.get("element_type").is_some());
assert!(json.get("props").is_some());
}
#[test]
fn test_serialize_set_prop_patch() {
let node_id = test_node_id();
let patch = Patch::set_prop(node_id, "color".to_string(), json!("red"));
let json = serde_json::to_value(&patch).unwrap();
assert_eq!(json["type"], "setProp");
assert!(json["id"].is_string());
assert_eq!(json["name"], "color");
assert_eq!(json["value"], "red");
}
#[test]
fn test_serialize_insert_patch() {
let parent_id = test_node_id();
let child_id = test_node_id();
let patch = Patch::insert(parent_id, child_id, None);
let json = serde_json::to_value(&patch).unwrap();
assert_eq!(json["type"], "insert");
assert!(json.get("parentId").is_some() || json.get("parent_id").is_some());
assert!(json.get("id").is_some());
let before_field = json.get("beforeId").or_else(|| json.get("before_id"));
assert!(before_field.is_some());
}
#[test]
fn test_deserialize_patch() {
let json = json!({
"type": "remove",
"id": "42"
});
let patch: Patch = serde_json::from_value(json).unwrap();
match patch {
Patch::Remove { id } => {
assert_eq!(id, "42");
}
_ => panic!("Expected Remove patch"),
}
}
#[test]
fn test_remove_prop_patch() {
let node_id = test_node_id();
let patch = Patch::remove_prop(node_id, "color".to_string());
match patch {
Patch::RemoveProp { id, name } => {
assert!(!id.is_empty());
assert_eq!(name, "color");
}
_ => panic!("Expected RemoveProp patch"),
}
}
#[test]
fn test_serialize_remove_prop_patch() {
let node_id = test_node_id();
let patch = Patch::remove_prop(node_id, "color".to_string());
let json = serde_json::to_value(&patch).unwrap();
assert_eq!(json["type"], "removeProp");
assert!(json["id"].is_string());
assert_eq!(json["name"], "color");
}
#[test]
fn test_deserialize_remove_prop_patch() {
let json = json!({
"type": "removeProp",
"id": "99",
"name": "color"
});
let patch: Patch = serde_json::from_value(json).unwrap();
match patch {
Patch::RemoveProp { id, name } => {
assert_eq!(id, "99");
assert_eq!(name, "color");
}
_ => panic!("Expected RemoveProp patch"),
}
}
#[test]
fn test_create_patch_wire_format_unchanged_by_arc_wrap() {
let node_id = test_node_id();
let props = indexmap! {
"text".to_string() => json!("Hello"),
"color".to_string() => json!("red"),
};
let patch = Patch::create(node_id, "Text".to_string(), Arc::new(props));
let json_str = serde_json::to_string(&patch).unwrap();
let json_val: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let props_val = json_val.get("props").expect("missing props field");
assert!(
props_val.is_object(),
"props must serialize as a bare JSON object, got: {}",
props_val
);
assert_eq!(props_val.get("text"), Some(&json!("Hello")));
assert_eq!(props_val.get("color"), Some(&json!("red")));
let obj = props_val.as_object().unwrap();
assert_eq!(obj.len(), 2, "props object grew extra fields: {:?}", obj);
}
#[test]
fn test_create_patch_serde_roundtrip() {
let node_id = test_node_id();
let props = indexmap! {
"text".to_string() => json!("Hello"),
"count".to_string() => json!(42),
"flag".to_string() => json!(true),
"nested".to_string() => json!({"inner": [1, 2, 3]}),
};
let original = Patch::create(node_id, "Text".to_string(), Arc::new(props));
let json_str = serde_json::to_string(&original).unwrap();
let restored: Patch = serde_json::from_str(&json_str).unwrap();
match restored {
Patch::Create {
props: restored_props,
element_type,
..
} => {
assert_eq!(element_type, "Text");
assert_eq!(restored_props.get("text"), Some(&json!("Hello")));
assert_eq!(restored_props.get("count"), Some(&json!(42)));
assert_eq!(restored_props.get("flag"), Some(&json!(true)));
assert_eq!(
restored_props.get("nested"),
Some(&json!({"inner": [1, 2, 3]}))
);
}
_ => panic!("Expected Create patch after round-trip"),
}
}
#[test]
fn test_create_patch_exact_json_bytes() {
use hypen_engine::reconcile::node_id_str;
let mut sm: slotmap::SlotMap<NodeId, ()> = slotmap::SlotMap::with_key();
let id = sm.insert(());
let id_str = node_id_str(id);
let props = indexmap! {
"text".to_string() => json!("Hi"),
};
let patch = Patch::create(id, "Text".to_string(), Arc::new(props));
let expected = serde_json::json!({
"type": "create",
"id": id_str,
"elementType": "Text",
"props": { "text": "Hi" },
});
let actual: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&patch).unwrap()).unwrap();
assert_eq!(
actual, expected,
"Patch::Create wire format drifted — renderers will break"
);
}
#[test]
fn test_node_id_str_stable_and_unique() {
use hypen_engine::reconcile::node_id_str;
use std::collections::HashSet;
let mut sm: slotmap::SlotMap<NodeId, ()> = slotmap::SlotMap::with_key();
let ids: Vec<NodeId> = (0..32).map(|_| sm.insert(())).collect();
for &id in &ids {
let first = node_id_str(id);
let second = node_id_str(id);
assert_eq!(first, second, "node_id_str must be deterministic per id");
}
let seen: HashSet<String> = ids.iter().copied().map(node_id_str).collect();
assert_eq!(
seen.len(),
ids.len(),
"different NodeIds collided to the same string"
);
}