use super::super::fixtures::make_graph;
use crate::runtime::xyflow::apply::{
XyFlowDimensionAttribute, XyFlowDimensionsSetAttributes, XyFlowEdgeChange, XyFlowEdgeElement,
XyFlowNodeChange, XyFlowNodeElement, apply_edge_changes, apply_graph_changes,
apply_node_changes, apply_xyflow_edge_changes, apply_xyflow_node_changes,
};
use crate::runtime::xyflow::changes::{EdgeChange, NodeChange, NodeGraphChanges};
use jellyflow_core::core::{CanvasPoint, CanvasSize, EdgeId, EdgeKind, NodeId, NodeOrigin};
#[test]
fn apply_node_changes_removes_ports_and_incident_edges() {
let (mut g0, a, b, out_port, in_port, eid) = make_graph();
let report = apply_node_changes(&mut g0, &[NodeChange::Remove { id: a }]);
assert!(report.did_change());
assert_eq!(report.ignored(), 0);
assert!(!g0.nodes.contains_key(&a));
assert!(g0.nodes.contains_key(&b));
assert!(!g0.ports.contains_key(&out_port));
assert!(g0.ports.contains_key(&in_port));
assert!(!g0.edges.contains_key(&eid));
}
#[test]
fn apply_node_changes_updates_origin_and_ignores_missing() {
let (mut g0, a, _b, _out_port, _in_port, _eid) = make_graph();
let missing = NodeId::new();
let report = apply_node_changes(
&mut g0,
&[
NodeChange::Position {
id: a,
position: CanvasPoint { x: 12.0, y: 24.0 },
},
NodeChange::Origin {
id: a,
origin: Some(NodeOrigin { x: 0.5, y: 0.25 }),
},
NodeChange::Origin {
id: missing,
origin: Some(NodeOrigin { x: 1.0, y: 1.0 }),
},
],
);
assert!(report.did_change());
assert_eq!(report.ignored(), 1);
assert_eq!(
g0.nodes.get(&a).unwrap().pos,
CanvasPoint { x: 12.0, y: 24.0 }
);
assert_eq!(
g0.nodes.get(&a).unwrap().origin,
Some(NodeOrigin { x: 0.5, y: 0.25 })
);
}
#[test]
fn apply_edge_changes_updates_kind_and_ignores_missing() {
let (mut g0, _a, _b, _out_port, _in_port, eid) = make_graph();
let missing = EdgeId::new();
let report = apply_edge_changes(
&mut g0,
&[
EdgeChange::Kind {
id: eid,
kind: EdgeKind::Exec,
},
EdgeChange::Hidden {
id: eid,
hidden: true,
},
EdgeChange::InteractionWidth {
id: eid,
interaction_width: Some(30.0),
},
EdgeChange::Remove { id: missing },
],
);
assert!(report.did_change());
assert_eq!(report.ignored(), 1);
assert_eq!(g0.edges.get(&eid).unwrap().kind, EdgeKind::Exec);
assert!(g0.edges.get(&eid).unwrap().hidden);
assert_eq!(g0.edges.get(&eid).unwrap().interaction_width, Some(30.0));
}
#[test]
fn node_update_changes_apply_and_transaction_paths_agree() {
let (g0, a, _b, _out_port, _in_port, _eid) = make_graph();
let changes = NodeGraphChanges::from_parts(
vec![
NodeChange::Position {
id: a,
position: CanvasPoint { x: 12.0, y: 24.0 },
},
NodeChange::Origin {
id: a,
origin: Some(NodeOrigin { x: 0.5, y: 0.25 }),
},
NodeChange::Selectable {
id: a,
selectable: Some(false),
},
NodeChange::Size {
id: a,
size: Some(CanvasSize {
width: 88.0,
height: 44.0,
}),
},
NodeChange::Hidden {
id: a,
hidden: true,
},
],
Vec::new(),
);
let mut applied = g0.clone();
let report = apply_graph_changes(&mut applied, &changes);
let tx = changes.to_transaction(&g0).expect("node update tx");
let mut transacted = g0.clone();
tx.apply_to(&mut transacted)
.expect("node update tx applies");
assert_eq!(report.applied(), 5);
assert_eq!(report.ignored(), 0);
let applied_node = applied.nodes.get(&a).expect("applied node");
let transacted_node = transacted.nodes.get(&a).expect("transacted node");
assert_eq!(applied_node.pos, transacted_node.pos);
assert_eq!(applied_node.origin, transacted_node.origin);
assert_eq!(applied_node.selectable, transacted_node.selectable);
assert_eq!(applied_node.size, transacted_node.size);
assert_eq!(applied_node.hidden, transacted_node.hidden);
}
#[test]
fn edge_update_changes_apply_and_transaction_paths_agree() {
let (g0, _a, _b, out_port, in_port, eid) = make_graph();
let replacement_from = out_port;
let replacement_to = in_port;
let changes = NodeGraphChanges::from_parts(
Vec::new(),
vec![
EdgeChange::Kind {
id: eid,
kind: EdgeKind::Exec,
},
EdgeChange::Hidden {
id: eid,
hidden: true,
},
EdgeChange::InteractionWidth {
id: eid,
interaction_width: Some(32.0),
},
EdgeChange::Endpoints {
id: eid,
from: replacement_from,
to: replacement_to,
},
],
);
let mut applied = g0.clone();
let report = apply_graph_changes(&mut applied, &changes);
let tx = changes.to_transaction(&g0).expect("edge update tx");
let mut transacted = g0.clone();
tx.apply_to(&mut transacted)
.expect("edge update tx applies");
assert_eq!(report.applied(), 4);
assert_eq!(report.ignored(), 0);
let applied_edge = applied.edges.get(&eid).expect("applied edge");
let transacted_edge = transacted.edges.get(&eid).expect("transacted edge");
assert_eq!(applied_edge.kind, transacted_edge.kind);
assert_eq!(applied_edge.hidden, transacted_edge.hidden);
assert_eq!(
applied_edge.interaction_width,
transacted_edge.interaction_width
);
assert_eq!(applied_edge.from, transacted_edge.from);
assert_eq!(applied_edge.to, transacted_edge.to);
}
#[test]
fn apply_xyflow_node_changes_preserves_react_ordering_semantics() {
let (g0, a, b, _out_port, _in_port, _eid) = make_graph();
let replacement = node_element(&g0, a);
let inserted = node_element(&g0, NodeId::new());
let appended = node_element(&g0, NodeId::new());
let nodes = vec![node_element(&g0, a), node_element(&g0, b)];
let updated = apply_xyflow_node_changes(
&[
XyFlowNodeChange::Position {
id: a,
position: Some(CanvasPoint { x: 10.0, y: 20.0 }),
position_absolute: None,
dragging: Some(true),
},
XyFlowNodeChange::Replace {
id: a,
item: replacement.clone(),
},
XyFlowNodeChange::Select {
id: a,
selected: true,
},
XyFlowNodeChange::Position {
id: b,
position: Some(CanvasPoint { x: 99.0, y: 100.0 }),
position_absolute: None,
dragging: None,
},
XyFlowNodeChange::Remove { id: b },
XyFlowNodeChange::Select {
id: b,
selected: true,
},
XyFlowNodeChange::Add {
item: inserted.clone(),
index: Some(0),
},
XyFlowNodeChange::Add {
item: appended.clone(),
index: None,
},
],
&nodes,
);
let ids: Vec<NodeId> = updated.iter().map(|node| node.id).collect();
assert_eq!(ids, vec![inserted.id, replacement.id, appended.id]);
assert_eq!(updated[1].node.pos, replacement.node.pos);
assert_eq!(updated[1].selected, None);
assert_eq!(updated[1].dragging, None);
}
#[test]
fn apply_xyflow_node_changes_updates_ui_state_dimensions_and_position() {
let (g0, a, _b, _out_port, _in_port, _eid) = make_graph();
let nodes = vec![node_element(&g0, a)];
let updated = apply_xyflow_node_changes(
&[
XyFlowNodeChange::Select {
id: a,
selected: true,
},
XyFlowNodeChange::Position {
id: a,
position: Some(CanvasPoint { x: 12.0, y: 24.0 }),
position_absolute: Some(CanvasPoint { x: 120.0, y: 240.0 }),
dragging: Some(true),
},
XyFlowNodeChange::Dimensions {
id: a,
dimensions: Some(CanvasSize {
width: 100.0,
height: 80.0,
}),
resizing: Some(true),
set_attributes: Some(XyFlowDimensionsSetAttributes::Bool(true)),
},
XyFlowNodeChange::Dimensions {
id: a,
dimensions: Some(CanvasSize {
width: 120.0,
height: 90.0,
}),
resizing: Some(false),
set_attributes: Some(XyFlowDimensionsSetAttributes::Attribute(
XyFlowDimensionAttribute::Width,
)),
},
XyFlowNodeChange::Dimensions {
id: a,
dimensions: None,
resizing: Some(true),
set_attributes: Some(XyFlowDimensionsSetAttributes::Attribute(
XyFlowDimensionAttribute::Height,
)),
},
],
&nodes,
);
let node = &updated[0];
assert_eq!(node.selected, Some(true));
assert_eq!(node.dragging, Some(true));
assert_eq!(node.resizing, Some(true));
assert_eq!(node.node.pos, CanvasPoint { x: 12.0, y: 24.0 });
assert_eq!(
node.measured,
Some(CanvasSize {
width: 120.0,
height: 90.0,
})
);
assert_eq!(node.width, Some(120.0));
assert_eq!(node.height, Some(80.0));
}
#[test]
fn apply_xyflow_node_changes_accepts_xyflow_camel_case_json_fields() {
let (g0, a, _b, _out_port, _in_port, _eid) = make_graph();
let change: XyFlowNodeChange = serde_json::from_value(serde_json::json!({
"type": "dimensions",
"id": a,
"dimensions": {
"width": 64.0,
"height": 48.0,
},
"setAttributes": "height",
"resizing": true,
}))
.expect("xyflow dimension change json");
let updated = apply_xyflow_node_changes(&[change], &[node_element(&g0, a)]);
assert_eq!(updated[0].width, None);
assert_eq!(updated[0].height, Some(48.0));
assert_eq!(updated[0].resizing, Some(true));
let encoded = serde_json::to_value(XyFlowNodeChange::Position {
id: a,
position: None,
position_absolute: Some(CanvasPoint { x: 1.0, y: 2.0 }),
dragging: None,
})
.expect("xyflow position json");
assert!(encoded.get("positionAbsolute").is_some());
assert!(encoded.get("position_absolute").is_none());
}
#[test]
fn apply_xyflow_edge_changes_preserves_react_ordering_semantics() {
let (g0, _a, _b, _out_port, _in_port, eid) = make_graph();
let second = EdgeId::new();
let removed = EdgeId::new();
let inserted = EdgeId::new();
let replacement = edge_element(&g0, eid);
let edges = vec![
edge_element(&g0, eid),
edge_element(&g0, second),
edge_element(&g0, removed),
];
let inserted_edge = edge_element(&g0, inserted);
let updated = apply_xyflow_edge_changes(
&[
XyFlowEdgeChange::Replace {
id: eid,
item: replacement.clone(),
},
XyFlowEdgeChange::Select {
id: eid,
selected: true,
},
XyFlowEdgeChange::Select {
id: second,
selected: true,
},
XyFlowEdgeChange::Remove { id: removed },
XyFlowEdgeChange::Select {
id: removed,
selected: true,
},
XyFlowEdgeChange::Add {
item: inserted_edge.clone(),
index: Some(1),
},
],
&edges,
);
let ids: Vec<EdgeId> = updated.iter().map(|edge| edge.id).collect();
assert_eq!(ids, vec![replacement.id, inserted_edge.id, second]);
assert_eq!(updated[0].selected, None);
assert_eq!(updated[2].selected, Some(true));
}
fn node_element(g: &jellyflow_core::core::Graph, id: NodeId) -> XyFlowNodeElement {
let node = g.nodes.values().next().expect("fixture node").clone();
XyFlowNodeElement::new(id, node)
}
fn edge_element(g: &jellyflow_core::core::Graph, id: EdgeId) -> XyFlowEdgeElement {
let edge = g.edges.values().next().expect("fixture edge").clone();
XyFlowEdgeElement::new(id, edge)
}