flowrs-engine/crates/flowrs-core/tests/fragment.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

184 lines
5.8 KiB
Rust

//! Copy/paste as a graph operation.
//!
//! The editor had ⌘D duplicate and no clipboard. Duplicate covers copying within one
//! graph; it cannot move a fragment between documents, between subgraph levels, or out of
//! the app. It also had a bug this API is shaped to avoid: it copied a node's type and
//! properties but not its `group`, and `add_node` creates every node with `group: None`,
//! so a duplicate made inside a subgraph silently appeared at the top level.
use flowrs_core::graph::ir::{FRAGMENT_VERSION, WireMode};
use flowrs_core::{Graph, NodeId};
/// `a -> mul <- b`, `mul -> print`.
fn chain() -> (Graph, Vec<NodeId>) {
let mut g = Graph::new();
let a = g.add_node("IntLiteral", 0.0, 0.0);
let b = g.add_node("IntLiteral", 0.0, 100.0);
let mul = g.add_node("Multiply", 200.0, 50.0);
let print = g.add_node("PrintLn", 400.0, 50.0);
g.get_node_mut(a)
.unwrap()
.properties
.insert("value".into(), "7".into());
g.add_wire(a, "value", mul, "a", WireMode::Copy).unwrap();
g.add_wire(b, "value", mul, "b", WireMode::Copy).unwrap();
g.add_wire(mul, "result", print, "input", WireMode::Move)
.unwrap();
(g, vec![a, b, mul, print])
}
#[test]
fn a_fragment_carries_the_wiring_between_its_own_nodes() {
let (g, ids) = chain();
let f = g.extract_fragment(&ids);
assert_eq!(f.version, FRAGMENT_VERSION);
assert_eq!(f.nodes.len(), 4);
assert_eq!(
f.wires.len(),
3,
"all three wires are internal to this selection"
);
}
#[test]
fn a_wire_with_one_end_outside_the_selection_is_dropped() {
let (g, ids) = chain();
// Just the two literals and the Multiply — the wire on to PrintLn leaves the set.
let f = g.extract_fragment(&ids[0..3]);
assert_eq!(f.nodes.len(), 3);
assert_eq!(
f.wires.len(),
2,
"the mul -> print wire has no destination in this fragment and must not be carried",
);
}
#[test]
fn pasting_allocates_fresh_ids_and_leaves_the_original_alone() {
let (mut g, ids) = chain();
let f = g.extract_fragment(&ids);
let before: Vec<NodeId> = {
let mut v: Vec<NodeId> = g.nodes.keys().copied().collect();
v.sort();
v
};
let new = g.insert_fragment(&f, None, 40.0, 40.0);
assert_eq!(new.len(), 4);
assert_eq!(g.nodes.len(), 8, "paste adds, it does not replace");
for id in &before {
assert!(
g.nodes.contains_key(id),
"original node {id:?} must survive the paste"
);
}
for id in &new {
assert!(
!before.contains(id),
"pasted node {id:?} reused an existing id"
);
}
}
#[test]
fn pasted_nodes_keep_their_properties_and_internal_wiring() {
let (mut g, ids) = chain();
let f = g.extract_fragment(&ids);
let new = g.insert_fragment(&f, None, 40.0, 40.0);
let pasted_literal = new
.iter()
.filter_map(|id| g.get_node(*id))
.find(|n| n.properties.get("value").map(String::as_str) == Some("7"));
assert!(
pasted_literal.is_some(),
"a literal's value must survive the round trip"
);
// Three wires among the pasted four, and none of them crossing back to the originals.
let pasted: std::collections::HashSet<NodeId> = new.iter().copied().collect();
let internal = g
.wires
.values()
.filter(|w| pasted.contains(&w.from_node) && pasted.contains(&w.to_node))
.count();
assert_eq!(
internal, 3,
"the fragment's wiring must be rebuilt among the copies"
);
let crossing = g
.wires
.values()
.filter(|w| pasted.contains(&w.from_node) != pasted.contains(&w.to_node))
.count();
assert_eq!(
crossing, 0,
"a paste must not wire itself to the original nodes"
);
}
#[test]
fn a_paste_is_offset_so_it_does_not_land_exactly_on_the_original() {
let (mut g, ids) = chain();
let f = g.extract_fragment(&ids);
let origin = g.get_node(ids[0]).unwrap().position;
let new = g.insert_fragment(&f, None, 40.0, 40.0);
let moved = g.get_node(new[0]).unwrap().position;
assert_eq!(
(moved.0 - origin.0, moved.1 - origin.1),
(40.0, 40.0),
"a copy landing exactly on its original is indistinguishable from nothing happening",
);
}
/// The bug that motivated putting `group` in this API's signature at all.
#[test]
fn a_fragment_lands_in_the_level_it_is_pasted_into() {
let (mut g, ids) = chain();
let f = g.extract_fragment(&ids);
let gid = flowrs_core::graph::ir::GroupId(1);
g.groups.insert(
gid,
flowrs_core::graph::ir::Group {
id: gid,
name: "inner".into(),
parent: None,
position: (0.0, 0.0),
},
);
let new = g.insert_fragment(&f, Some(gid), 0.0, 0.0);
for id in &new {
assert_eq!(
g.get_node(*id).unwrap().group,
Some(gid),
"every pasted node belongs to the level it was pasted into",
);
}
// And the source nodes are untouched at the top level.
for id in &ids {
assert_eq!(g.get_node(*id).unwrap().group, None);
}
}
#[test]
fn an_empty_selection_yields_an_empty_fragment_rather_than_the_whole_graph() {
let (g, _) = chain();
let f = g.extract_fragment(&[]);
assert!(f.nodes.is_empty() && f.wires.is_empty());
}
#[test]
fn a_fragment_survives_json() {
let (mut g, ids) = chain();
let f = g.extract_fragment(&ids);
let json = serde_json::to_string(&f).expect("a fragment must serialize");
let back: flowrs_core::graph::ir::Fragment =
serde_json::from_str(&json).expect("and come back");
let new = g.insert_fragment(&back, None, 10.0, 10.0);
assert_eq!(
new.len(),
4,
"the clipboard round trip is JSON, so this is the real path"
);
}