use super::*; #[test] fn state_bend_delayed_commit_after_same_view_selection_is_inert_and_live_commit_works() { let (mut app, _, relation, _) = setup(); let context = app.state_context().unwrap(); let next = app.project().relations[&relation].target(); let waypoints = vec![Position { x: 715., y: -425. }]; let delayed = Event::SetBends { context, relation, waypoints: waypoints.clone(), }; send(&mut app, Message::Select(next)); assert_eq!(app.selected, next); assert_eq!( app.state_context(), Some(context), "same-view selection does not retire the context stamp" ); let before = app.document.snapshot(); let camera = ( app.behavior.diagram.zoom, app.behavior.diagram.pan, app.behavior.diagram.fit, ); event(&mut app, delayed); assert_eq!(app.selected, next); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); assert_eq!(app.project(), &before.decoded.project); assert_eq!(app.document.bundle(), &before.bundle); assert!(!app.document.can_undo()); assert_eq!( ( app.behavior.diagram.zoom, app.behavior.diagram.pan, app.behavior.diagram.fit ), camera ); send(&mut app, Message::Select(relation)); let context = app.state_context().unwrap(); event( &mut app, Event::SetBends { context, relation, waypoints: waypoints.clone(), }, ); let mut expected = before.decoded.project.clone(); let route = expected .diagrams .get_mut(&context.diagram) .unwrap() .routes .iter_mut() .find(|route| route.edge == relation) .unwrap(); route.waypoints = waypoints; assert_eq!(app.project(), &expected); assert_source_model_agree(&app); send(&mut app, Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); assert!( !app.document.can_undo(), "live drag remains a single transaction" ); } #[test] fn state_bend_menu_carries_exact_segment_and_pin_without_changing_delete_action() { let (mut app, _, relation, _) = setup(); let context = app.state_context().unwrap(); let point = Position { x: 500., y: -400. }; event( &mut app, Event::ContextMenu { context, object: relation, placement: None, selection_target: None, anchor: iced::Point::ORIGIN, bend: Some(1), route_segment: Some((2, point)), }, ); let menu = crate::model_menu::popup(&app).unwrap(); let mut add = false; let mut remove = false; let mut delete = false; for entry in menu.entries { if let crate::context_menu::Entry::Action { message: Some(Message::Menu(crate::model_menu::Event::Invoke(_, message))), .. } = entry { match *message { Message::StateDiagram(Event::AddBend { relation: id, segment: Some((2, at)), .. }) => { assert_eq!(id, relation); assert_eq!(at, point); add = true; } Message::StateDiagram(Event::RemoveBend { relation: id, index: 1, .. }) => { assert_eq!(id, relation); remove = true; } Message::Delete => delete = true, _ => {} } } } assert!(add && remove && delete); } fn setup() -> (App, Id, Id, Id) { let mut f = fixture(); let diagram = create_view(&mut f).diagram; let mut project = f.app.project().clone(); let relation = project.relate(RelationKind::StateTransition, "Bend trial", f.leaf, f.b); let second = state_edit::create_view(&project, f.machine).unwrap(); project = prepare_command(&project, &second.command).unwrap().project; project .diagrams .get_mut(&diagram) .unwrap() .routes .push(EdgeRoute { edge: relation, waypoints: vec![ Position { x: 650., y: -400. }, Position { x: 300., y: -400. }, Position { x: 650., y: -400. }, ], label: Some(EdgeLabel { fraction: 0.35, offset: Position { x: 9., y: -23. }, }), source_attachment: Some(EdgeAttachment { side: PortAnchorSide::Top, offset: 0.234567, }), target_attachment: Some(EdgeAttachment { side: PortAnchorSide::Bottom, offset: 0.75, }), }); let mut app = App::with_project(project); send(&mut app, Message::RevealInDiagram(diagram, relation)); (app, diagram, relation, f.a) } #[test] fn state_bend_commands_preserve_other_fields_views_and_exact_history() { let (mut app, diagram, relation, _) = setup(); let baseline = app.document.snapshot(); let points = vec![Position { x: 715., y: 425. }]; let mut expected = app.project().clone(); expected.diagrams.get_mut(&diagram).unwrap().routes[0].waypoints = points.clone(); let context = app.state_context().unwrap(); event( &mut app, Event::SetBends { context, relation, waypoints: points.clone(), }, ); assert_eq!(app.project(), &expected); assert_source_model_agree(&app); let changed = app.document.snapshot(); let context = app.state_context().unwrap(); event( &mut app, Event::SetBends { context, relation, waypoints: points, }, ); assert!( Arc::ptr_eq(&changed, &app.document.snapshot()), "equal bends must not add history" ); send(&mut app, Message::Undo); assert_eq!(app.project(), &baseline.decoded.project); assert_eq!(app.document.bundle(), &baseline.bundle); assert!(!app.document.can_undo()); send(&mut app, Message::Redo); assert_eq!(app.project(), &expected); assert_eq!(app.document.bundle(), &changed.bundle); let context = app.state_context().unwrap(); event(&mut app, Event::ClearBends { context, relation }); expected.diagrams.get_mut(&diagram).unwrap().routes[0] .waypoints .clear(); assert_eq!(app.project(), &expected); let cleared = app.document.snapshot(); let context = app.state_context().unwrap(); event(&mut app, Event::ClearBends { context, relation }); assert!(Arc::ptr_eq(&cleared, &app.document.snapshot())); } #[test] fn state_bend_self_loop_add_move_remove_clear_preserves_project_and_source_history() { let (app, diagram, relation, _) = setup(); let mut project = app.project().clone(); let RelationBody::StateTransition { source, target, .. } = &mut project.relations.get_mut(&relation).unwrap().body else { panic!("State transition fixture") }; *target = *source; project.diagrams.get_mut(&diagram).unwrap().routes[0] .waypoints .clear(); oa_core::validate_candidate(&project).unwrap(); let mut app = App::with_project(project); send(&mut app, Message::RevealInDiagram(diagram, relation)); let baseline = app.document.snapshot(); let geometry = state_edit::bend_route(app.project(), diagram, relation).unwrap(); assert!(!geometry.custom_suspended); assert!(geometry.pins.is_empty()); assert!( geometry.points.len() >= 4, "actual external self-loop routing" ); let segment = 1; let point = Position { x: (geometry.points[segment].x + geometry.points[segment + 1].x) / 2., y: (geometry.points[segment].y + geometry.points[segment + 1].y) / 2., }; // Each operation must alter only the complete Project's waypoint list; // Undo and Redo must restore exact serialized source, not just geometry. fn check(app: &mut App, diagram: Id, mutation: Event, points: Vec) { let before = app.document.snapshot(); let mut expected = before.decoded.project.clone(); expected.diagrams.get_mut(&diagram).unwrap().routes[0].waypoints = points; event(app, mutation); assert_eq!(app.project(), &expected); assert_source_model_agree(app); let after = app.document.snapshot(); assert_ne!(after.bundle, before.bundle); send(app, Message::Undo); assert_eq!(app.project(), &before.decoded.project); assert_eq!(app.document.bundle(), &before.bundle); send(app, Message::Redo); assert_eq!(app.project(), &expected); assert_eq!(app.document.bundle(), &after.bundle); assert_source_model_agree(app); } let context = app.state_context().unwrap(); check( &mut app, diagram, Event::AddBend { context, relation, segment: Some((segment, point)), }, vec![point], ); let moved = Position { x: point.x + 45., y: point.y - 35., }; let context = app.state_context().unwrap(); check( &mut app, diagram, Event::SetBends { context, relation, waypoints: vec![moved], }, vec![moved], ); let context = app.state_context().unwrap(); check( &mut app, diagram, Event::RemoveBend { context, relation, index: 0, }, vec![], ); assert_eq!(app.project(), &baseline.decoded.project); send(&mut app, Message::Undo); let context = app.state_context().unwrap(); check( &mut app, diagram, Event::ClearBends { context, relation }, vec![], ); assert_eq!(app.project(), &baseline.decoded.project); assert_source_model_agree(&app); // Clear is a new source-preserving edit and may retain empty-list trivia. // Exact original bytes are restored by history, not by reauthoring a value. for _ in 0..3 { assert!(app.document.can_undo()); send(&mut app, Message::Undo); } assert_eq!(app.project(), &baseline.decoded.project); assert_eq!(app.document.bundle(), &baseline.bundle); assert!(!app.document.can_undo()); } #[test] fn state_bend_insertion_uses_captured_traversal_segment_and_removal_only_changes_pins() { let (app, diagram, relation, _) = setup(); let context = app.state_context().unwrap(); let p = app.project(); let geometry = state_edit::bend_route(p, diagram, relation).unwrap(); let original = &p.diagrams[&diagram].routes[0]; let reverse = geometry .points .windows(2) .position(|pair| pair[0] == original.waypoints[0] && pair[1] == original.waypoints[1]) .unwrap(); for (segment, expected_index) in [(reverse, 1), (reverse + 1, 2)] { let point = Position { x: 500., y: -400. }; let planned = state_edit::plan( p, context, &Event::AddBend { context, relation, segment: Some((segment, point)), }, ) .unwrap(); let actual = prepare_command(p, &planned.command).unwrap().project; let mut expected = p.clone(); expected.diagrams.get_mut(&diagram).unwrap().routes[0] .waypoints .insert(expected_index, point); assert_eq!(actual, expected); let removed = state_edit::plan( &actual, context, &Event::RemoveBend { context, relation, index: expected_index, }, ) .unwrap(); assert_eq!( prepare_command(&actual, &removed.command).unwrap().project, *p ); } let default = state_edit::plan( p, context, &Event::AddBend { context, relation, segment: None, }, ) .unwrap(); let Command::SetEdgeRoute { route, .. } = default.command else { panic!("route command") }; assert_eq!(route.waypoints.len(), original.waypoints.len() + 1); assert_eq!(route.source_attachment, original.source_attachment); assert_eq!(route.target_attachment, original.target_attachment); assert_eq!(route.label, original.label); } #[test] fn state_bend_stale_nonfinite_capacity_hidden_and_folded_requests_are_inert() { let (mut app, diagram, relation, composite) = setup(); let context = app.state_context().unwrap(); let baseline = app.document.snapshot(); for mutation in [ Event::SetBends { context: Context { epoch: context.epoch + 1, ..context }, relation, waypoints: vec![], }, Event::SetBends { context, relation, waypoints: vec![Position { x: f32::NAN, y: 0. }], }, Event::SetBends { context, relation, waypoints: vec![Position { x: 0., y: 0. }; 129], }, Event::AddBend { context, relation, segment: Some((usize::MAX, Position { x: 0., y: 0. })), }, Event::RemoveBend { context, relation, index: usize::MAX, }, ] { event(&mut app, mutation); assert!(Arc::ptr_eq(&baseline, &app.document.snapshot())); } let mut p = app.project().clone(); p.diagrams.get_mut(&diagram).unwrap().routes[0].waypoints = vec![Position { x: 500., y: -400. }; 128]; assert!( state_edit::plan( &p, context, &Event::AddBend { context, relation, segment: Some((1, Position { x: 500., y: -400. })) } ) .is_err() ); let mut hidden = app.project().clone(); hidden .diagrams .get_mut(&diagram) .unwrap() .view_options .hidden_edges .push(relation); assert!(state_edit::bend_route(&hidden, diagram, relation).is_none()); assert!(state_edit::plan(&hidden, context, &Event::ClearBends { context, relation }).is_err()); let owner = node(&app, diagram, composite); let mut folded = app.project().clone(); folded .diagrams .get_mut(&diagram) .unwrap() .nodes .get_mut(&owner) .unwrap() .collapsed = true; assert!(state_edit::bend_route(&folded, diagram, relation).is_none()); assert!(state_edit::plan(&folded, context, &Event::ClearBends { context, relation }).is_err()); let mut internal = app.project().clone(); internal.diagrams.get_mut(&diagram).unwrap().routes.clear(); if let RelationBody::StateTransition { source, target, transition_kind, .. } = &mut internal.relations.get_mut(&relation).unwrap().body { *target = *source; *transition_kind = TransitionKind::Internal; } assert!(state_edit::bend_route(&internal, diagram, relation).is_none()); send( &mut app, Message::Field("/name".into(), "Unapplied bend owner name".into()), ); let draft = app.draft.as_ref().unwrap().value.clone(); event(&mut app, Event::ClearBends { context, relation }); assert!(Arc::ptr_eq(&baseline, &app.document.snapshot())); assert_eq!(app.draft.as_ref().unwrap().value, draft); }