use oa_core::{diagram_edit::translate_placements, source_project::*, *}; use std::collections::BTreeSet; fn point(x: f32, y: f32) -> Position { Position { x, y } } fn route(edge: Id, x: f32, y: f32) -> EdgeRoute { EdgeRoute { edge, source_attachment: Some(EdgeAttachment { side: PortAnchorSide::Bottom, offset: 0.2, }), target_attachment: Some(EdgeAttachment { side: PortAnchorSide::Top, offset: 0.8, }), waypoints: vec![point(x, y)], label: Some(EdgeLabel { fraction: 0.4, offset: point(3., -9.), }), } } struct Fixture { project: Project, diagram: Id, a: Id, b: Id, child: Id, foreign: Id, } fn fixture() -> Fixture { let mut p = Project::empty(); let a = p.add_element(Kind::Activity, Layer::Oa, "A", None).unwrap(); let b = p.add_element(Kind::Activity, Layer::Oa, "B", None).unwrap(); let c = p.add_element(Kind::Activity, Layer::Oa, "C", None).unwrap(); let child = p .add_element(Kind::Activity, Layer::Oa, "Child", Some(a)) .unwrap(); let internal = p.relate(RelationKind::Interaction, "Internal", a, b); let external = p.relate(RelationKind::Interaction, "External", b, c); let d = p.ensure_diagram(Layer::Oa); let na = p.place(d, a, point(10., 20.)); let nb = p.place(d, b, point(400., 20.)); let nc = p.place(d, child, point(30., 80.)); p.place(d, c, point(800., 20.)); p.diagrams.get_mut(&d).unwrap().routes = vec![route(internal, 320., 150.), route(external, 720., 170.)]; p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&na) .unwrap() .collapsed = true; let other = Id::new_v4(); let mut other_view = p.diagrams[&d].clone(); other_view.id = other; other_view.alias = "other_view".into(); other_view.nodes.clear(); other_view.routes.clear(); p.diagrams.insert(other, other_view); let foreign = p.place(other, a, point(-40., -50.)); validate_candidate(&p).unwrap(); Fixture { project: p, diagram: d, a: na, b: nb, child: nc, foreign, } } #[test] fn simultaneous_siblings_translate_internal_pins_once_and_preserve_complete_project() { let f = fixture(); let before = f.project.clone(); let command = translate_placements( &before, f.diagram, &BTreeSet::from([f.a, f.b]), point(40., 60.), ) .unwrap(); assert_eq!( before, f.project, "command construction must not mutate input" ); // Independently enumerated expected edits, without the projection helpers. let mut expected = before.clone(); let view = expected.diagrams.get_mut(&f.diagram).unwrap(); view.nodes.get_mut(&f.a).unwrap().position = point(50., 80.); view.nodes.get_mut(&f.b).unwrap().position = point(440., 80.); view.nodes.get_mut(&f.child).unwrap().position = point(70., 140.); view.routes[0].waypoints = vec![point(360., 210.)]; let mut session = Session::new(before.clone()); session.execute(command.clone()).unwrap(); assert_eq!(session.project(), &expected); session.undo(); assert_eq!(session.project(), &before); assert!( !session.can_undo(), "one movement must be one history entry" ); session.redo(); assert_eq!(session.project(), &expected); assert!(!session.can_redo()); // Independent one-node moves cannot see the shared endpoint displacement. let mut sequential = Session::new(before.clone()); for (node, position) in [(f.a, point(50., 80.)), (f.b, point(440., 80.))] { sequential .execute(Command::MoveNode { diagram: f.diagram, node, position, with_descendants: true, }) .unwrap(); } assert_eq!( sequential.project().diagrams[&f.diagram].routes[0], before.diagrams[&f.diagram].routes[0] ); assert_ne!(sequential.project(), &expected); let json = serde_json::to_string(&expected).unwrap(); assert_eq!(serde_json::from_str::(&json).unwrap(), expected); let mut bundle = format_bundle(&before); bundle .files .get_mut(&bundle.entry.clone()) .unwrap() .insert_str(0, "// preserve movement rationale\n"); let original = compile_bundle(&bundle, ProjectLimits::default()).unwrap(); let prepared = prepare_command(&before, &command).unwrap(); let updated = update_bundle_with_identity_map( &original, &prepared.project, &prepared.identity_map, ProjectLimits::default(), ) .unwrap(); assert_eq!(updated.decoded.project, expected); assert!(updated.bundle.files[&bundle.entry].starts_with("// preserve movement rationale\n")); let reread = compile_bundle(&updated.bundle, ProjectLimits::default()).unwrap(); assert_eq!(reread.decoded.project, expected); assert_eq!(reread.bundle, updated.bundle); } #[test] fn selecting_folded_ancestor_and_hidden_descendant_moves_each_once() { let f = fixture(); assert!( !projection::project_view(&f.project, f.diagram) .nodes .iter() .any(|n| n.id == f.child) ); let ancestor = translate_placements( &f.project, f.diagram, &BTreeSet::from([f.a]), point(-15., 25.), ) .unwrap(); let overlap = translate_placements( &f.project, f.diagram, &BTreeSet::from([f.a, f.child]), point(-15., 25.), ) .unwrap(); assert_eq!(overlap, ancestor); let after = prepare_command(&f.project, &overlap).unwrap().project; assert_eq!( after.diagrams[&f.diagram].nodes[&f.child].position, point(15., 105.) ); assert_eq!( after.diagrams[&f.diagram].routes, f.project.diagrams[&f.diagram].routes ); } #[test] fn invalid_movements_refuse_atomically_and_zero_has_no_history() { let f = fixture(); let selected = BTreeSet::from([f.a, f.b]); for (diagram, ids, delta) in [ (Id::new_v4(), selected.clone(), point(1., 1.)), (f.diagram, BTreeSet::new(), point(1., 1.)), ( f.diagram, BTreeSet::from([f.a, Id::new_v4()]), point(1., 1.), ), (f.diagram, BTreeSet::from([f.a, f.foreign]), point(0., 0.)), (f.diagram, selected.clone(), point(f32::NAN, 1.)), (f.diagram, selected.clone(), point(1., f32::INFINITY)), (f.diagram, selected.clone(), point(MAX_COORDINATE, 0.)), (f.diagram, selected.clone(), point(f32::MAX, f32::MAX)), ] { let before = f.project.clone(); assert!(translate_placements(&f.project, diagram, &ids, delta).is_err()); assert_eq!(f.project, before); } let mut bad_route = f.project.clone(); bad_route.diagrams.get_mut(&f.diagram).unwrap().routes[0].waypoints = vec![point(MAX_COORDINATE, 0.)]; validate_candidate(&bad_route).unwrap(); let snapshot = bad_route.clone(); assert!(translate_placements(&bad_route, f.diagram, &selected, point(1., 0.)).is_err()); assert_eq!(bad_route, snapshot); let zero = translate_placements(&f.project, f.diagram, &selected, point(0., -0.)).unwrap(); assert_eq!(zero, Command::Batch(vec![])); let mut session = Session::new(f.project.clone()); session.execute(zero).unwrap(); assert_eq!(session.project(), &f.project); assert!(!session.can_undo()); assert_eq!(session.revision(), 0); } #[test] fn typed_component_port_endpoints_follow_their_owner_placements() { let mut p = Project::empty(); let a = p .add_element(Kind::System, Layer::Sa, "System", None) .unwrap(); let b = p .add_element(Kind::SystemActor, Layer::Sa, "Actor", None) .unwrap(); let output = Id::new_v4(); let input = Id::new_v4(); for (id, owner, name, orientation) in [ (output, a, "Output", ComponentPortOrientation::Out), (input, b, "Input", ComponentPortOrientation::In), ] { let port = make_port( &p, id, owner, name, PortContract::Component { kind: ComponentPortKind::Flow, orientation, }, ) .unwrap(); p.elements.insert(id, port); } let edge = p.relate(RelationKind::BoundaryExchange, "Boundary", output, input); let d = p.ensure_diagram(Layer::Sa); let na = p.place(d, a, point(0., 0.)); let nb = p.place(d, b, point(400., 0.)); for (node, port, side, offset) in [ (na, output, PortAnchorSide::Bottom, 0.3), (nb, input, PortAnchorSide::Top, 0.7), ] { p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&node) .unwrap() .port_anchors .push(PortAnchor { port, side, offset }); } p.diagrams .get_mut(&d) .unwrap() .routes .push(route(edge, 300., 100.)); validate_candidate(&p).unwrap(); let command = translate_placements(&p, d, &BTreeSet::from([na, nb]), point(10., -20.)).unwrap(); let after = prepare_command(&p, &command).unwrap().project; let mut expected = p.clone(); let view = expected.diagrams.get_mut(&d).unwrap(); view.nodes.get_mut(&na).unwrap().position = point(10., -20.); view.nodes.get_mut(&nb).unwrap().position = point(410., -20.); view.routes[0].waypoints = vec![point(310., 80.)]; assert_eq!(after, expected); } #[test] fn allocation_movement_follows_visual_performers_and_same_performer_work_groups() { let mut p = Project::empty(); let performer_a = p .add_element(Kind::Entity, Layer::Oa, "Performer A", None) .unwrap(); let performer_b = p .add_element(Kind::Entity, Layer::Oa, "Performer B", None) .unwrap(); let mixed = p .add_element(Kind::Activity, Layer::Oa, "Mixed work", None) .unwrap(); let branch = p .add_element(Kind::Activity, Layer::Oa, "A work", Some(mixed)) .unwrap(); let leaf_a = p .add_element(Kind::Activity, Layer::Oa, "A leaf", Some(branch)) .unwrap(); let leaf_b = p .add_element(Kind::Activity, Layer::Oa, "B leaf", Some(mixed)) .unwrap(); p.relate(RelationKind::Allocation, "A performs", performer_a, leaf_a); p.relate(RelationKind::Allocation, "B performs", performer_b, leaf_b); let d = p.ensure_diagram(Layer::Oa); p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Allocation; let na = p.place(d, performer_a, point(0., 0.)); p.place(d, performer_b, point(600., 0.)); let nm = p.place(d, mixed, point(1000., 0.)); let branch_node = p.place(d, branch, point(20., 80.)); let leaf_node = p.place(d, leaf_a, point(40., 160.)); p.place(d, leaf_b, point(620., 80.)); validate_candidate(&p).unwrap(); // Mixed work retains semantic ownership of both branches, but its leaves // have different performers. A work is visually nested under Performer A, // with its own same-performer leaf nested inside it. let command = translate_placements(&p, d, &BTreeSet::from([na, branch_node]), point(15., -25.)).unwrap(); let after = prepare_command(&p, &command).unwrap().project; let mut expected = p.clone(); let view = expected.diagrams.get_mut(&d).unwrap(); view.nodes.get_mut(&na).unwrap().position = point(15., -25.); view.nodes.get_mut(&branch_node).unwrap().position = point(35., 55.); view.nodes.get_mut(&leaf_node).unwrap().position = point(55., 135.); assert_eq!(after, expected); // Moving the semantic parent does not move children displayed elsewhere. let command = translate_placements(&p, d, &BTreeSet::from([nm]), point(15., -25.)).unwrap(); let after = prepare_command(&p, &command).unwrap().project; let mut expected = p.clone(); expected .diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&nm) .unwrap() .position = point(1015., -25.); assert_eq!(after, expected); } #[test] fn traceability_movement_is_flat_even_for_a_semantic_parent() { let mut f = fixture(); f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Traceability; validate_candidate(&f.project).unwrap(); let command = translate_placements( &f.project, f.diagram, &BTreeSet::from([f.a]), point(40., 60.), ) .unwrap(); let after = prepare_command(&f.project, &command).unwrap().project; let mut expected = f.project.clone(); expected .diagrams .get_mut(&f.diagram) .unwrap() .nodes .get_mut(&f.a) .unwrap() .position = point(50., 80.); assert_eq!( after, expected, "flat view movement must preserve semantic descendants, routes, other views and all semantics" ); } #[test] fn state_entry_pins_follow_the_boundary_only_when_it_also_moves() { let mut p = Project::empty(); let machine = p .add_element(Kind::StateMachine, Layer::Oa, "Machine", None) .unwrap(); let region = p .add_element(Kind::Region, Layer::Oa, "Region", Some(machine)) .unwrap(); let composite = p .add_element(Kind::State, Layer::Oa, "Composite", Some(region)) .unwrap(); let inner = p .add_element(Kind::Region, Layer::Oa, "Inner", Some(composite)) .unwrap(); let entry = p .add_element(Kind::EntryPoint, Layer::Oa, "Entry", Some(inner)) .unwrap(); let leaf = p .add_element(Kind::State, Layer::Oa, "Leaf", Some(inner)) .unwrap(); let edge = p.relate(RelationKind::StateTransition, "Enter", entry, leaf); if let RelationBody::StateTransition { transition_kind, .. } = &mut p.relations.get_mut(&edge).unwrap().body { *transition_kind = TransitionKind::Local; } let d = p.ensure_diagram(Layer::Oa); p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::StateMachine { machine }; let boundary = p.place(d, composite, point(0., 0.)); let ne = p.place(d, entry, point(20., 80.)); let nl = p.place(d, leaf, point(100., 80.)); p.diagrams .get_mut(&d) .unwrap() .routes .push(route(edge, 90., 100.)); validate_candidate(&p).unwrap(); let endpoints = translate_placements(&p, d, &BTreeSet::from([ne, nl]), point(10., 20.)).unwrap(); let after = prepare_command(&p, &endpoints).unwrap().project; assert_eq!(after.diagrams[&d].routes, p.diagrams[&d].routes); let with_boundary = translate_placements(&p, d, &BTreeSet::from([boundary, ne, nl]), point(10., 20.)).unwrap(); let after = prepare_command(&p, &with_boundary).unwrap().project; assert_eq!( after.diagrams[&d].routes[0].waypoints, vec![point(100., 120.)] ); assert_eq!(after.diagrams[&d].nodes[&ne].position, point(30., 100.)); }