// Included in connection_edit::tests: exercise the real application event path. use oa_core::projection::project_view; fn allocation_fixture(layer: Layer, projection: ProjectionKind) -> (App, Id, Id, Id) { let mut project = Project::empty(); let (performer_kind, work_kind) = match layer { Layer::Oa => (Kind::Entity, Kind::Activity), Layer::Sa => (Kind::System, Kind::Function), }; let performer = project.add(performer_kind, "Assigned performer", 20., 20.); let work = project.add(work_kind, "Existing leaf work", 420., 20.); let diagram = project.ensure_diagram(layer); project.diagrams.get_mut(&diagram).unwrap().projection = projection; let reference_id = Id::new_v4(); let mut reference = project.diagrams[&diagram].clone(); reference.id = reference_id; reference.alias = project.fresh_alias("Reference view", "diagram", reference_id); reference.name = "Reference view".into(); reference.nodes.clear(); project.diagrams.insert(reference_id, reference); project.place(reference_id, performer, Position { x: 80., y: 100. }); project.place(reference_id, work, Position { x: 500., y: 300. }); let mut app = App::with_project(project); let _ = app.update(Message::Diagram(crate::app::Choice { id: diagram, label: app.project().diagrams[&diagram].name.clone(), })); (app, performer, work, diagram) } fn start_allocation(app: &mut App, performer: Id) -> Context { send(app, Event::Choose(Some(Tool::Allocation))); let context = app.route_context().unwrap(); assert!( app.connection .as_ref() .unwrap() .candidates .contains(&performer) ); send(app, Event::Endpoint(context, performer)); assert_eq!(app.connection.as_ref().unwrap().source, Some(performer)); context } #[test] fn allocation_projection_eligibility_requires_both_endpoint_categories() { for layer in [Layer::Oa, Layer::Sa] { for projection in [ ProjectionKind::Overview, ProjectionKind::Decomposition, ProjectionKind::Traceability, ProjectionKind::Allocation, ] { assert!(Tool::Allocation.available_in(layer, projection)); } for projection in [ ProjectionKind::FunctionalDataflow, ProjectionKind::Capabilities, ProjectionKind::StateMachine { machine: Id::new_v4(), }, ] { assert!(!Tool::Allocation.available_in(layer, projection)); } for projection in [ ProjectionKind::FunctionalDataflow, ProjectionKind::Capabilities, ] { let (mut app, _, _, _) = allocation_fixture(layer, projection); let before = app.document.bundle().clone(); send(&mut app, Event::Choose(Some(Tool::Allocation))); assert!(app.connection.is_none()); assert_eq!(app.document.bundle(), &before); } } // The new predicate retains the four established exchange-tool policies. assert!(Tool::Interaction.available_in(Layer::Oa, ProjectionKind::FunctionalDataflow)); assert!(Tool::FunctionalExchange.available_in(Layer::Sa, ProjectionKind::FunctionalDataflow)); assert!(Tool::CommunicationMean.available_in(Layer::Oa, ProjectionKind::Capabilities)); assert!(Tool::BoundaryExchange.available_in(Layer::Sa, ProjectionKind::Capabilities)); assert!(!Tool::FunctionalExchange.available_in(Layer::Oa, ProjectionKind::Overview)); } #[test] fn allocation_canvas_commits_only_one_relation_with_exact_source_undo_redo() { use oa_core::source_project::{ProjectLimits, compile_bundle}; for layer in [Layer::Oa, Layer::Sa] { for projection in [ ProjectionKind::Overview, ProjectionKind::Decomposition, ProjectionKind::Traceability, ProjectionKind::Allocation, ] { let (mut app, performer, work, diagram) = allocation_fixture(layer, projection); let original = app.project().clone(); let before = app.document.bundle().clone(); let revision = app.document.revision(); let context = start_allocation(&mut app, performer); assert_eq!(app.document.bundle(), &before); send(&mut app, Event::Endpoint(context, work)); assert!(app.connection.is_none()); assert_eq!(app.document.revision(), revision + 1); let relation = &app.project().relations[&app.selected]; let name = "Allocation: Assigned performer → Existing leaf work"; let expected_relation = Relation { id: relation.id, alias: original.fresh_alias(name, "relation", relation.id), name: name.into(), description: String::new(), body: RelationBody::new(RelationKind::Allocation, performer, work), }; let relation_id = relation.id; let mut expected = original.clone(); expected.relations.insert(relation_id, expected_relation); assert_eq!(app.project(), &expected, "Only the new relation may change"); assert_eq!(app.active_diagram, Some(diagram)); assert!( app.status .contains("Allocated Existing leaf work to Assigned performer") ); let projected = project_view(app.project(), diagram); if projection == ProjectionKind::Allocation { assert!(!projected.edges.iter().any(|edge| edge.id == relation_id)); let owner = projected .nodes .iter() .find(|node| node.element == performer) .unwrap(); let child = projected .nodes .iter() .find(|node| node.element == work) .unwrap(); assert_eq!(child.visual_parent, Some(owner.id)); } else { assert!(projected.edges.iter().any(|edge| edge.id == relation_id)); } let after = app.document.bundle().clone(); assert_eq!( compile_bundle(&after, ProjectLimits::default()) .unwrap() .decoded .project, expected ); send(&mut app, Event::Endpoint(context, work)); assert_eq!( app.document.revision(), revision + 1, "Repeated release is not another transaction" ); let _ = app.update(Message::Undo); assert_eq!(app.project(), &original); assert_eq!(app.document.bundle(), &before); let _ = app.update(Message::Redo); assert_eq!(app.project(), &expected); assert_eq!(app.document.bundle(), &after); } } } #[test] fn allocation_candidates_reject_nonleaf_occupied_cross_layer_port_and_hidden_work() { for layer in [Layer::Oa, Layer::Sa] { let (base, performer, work, diagram) = allocation_fixture(layer, ProjectionKind::Traceability); let mut project = base.project().clone(); let work_kind = project.elements[&work].kind(); let composite = project .add_element(work_kind, layer, "Composite work", None) .unwrap(); project .add_element(work_kind, layer, "Child work", Some(composite)) .unwrap(); let occupied = project .add_element(work_kind, layer, "Occupied work", None) .unwrap(); let allocation = Tool::Allocation .command(&project, performer, occupied) .unwrap(); project = prepare_command(&project, &allocation).unwrap().project; let hidden = project .add_element(work_kind, layer, "Unplaced work", None) .unwrap(); let other_layer = if layer == Layer::Oa { Layer::Sa } else { Layer::Oa }; let other_kind = if other_layer == Layer::Oa { Kind::Activity } else { Kind::Function }; let foreign = project .add_element(other_kind, other_layer, "Other perspective", None) .unwrap(); let foreign_performer_kind = if other_layer == Layer::Oa { Kind::Actor } else { Kind::SystemActor }; let foreign_performer = project .add_element(foreign_performer_kind, other_layer, "Other performer", None) .unwrap(); for (index, id) in [composite, occupied, foreign, foreign_performer] .into_iter() .enumerate() { project.place( diagram, id, Position { x: index as f32 * 300., y: 400., }, ); } let port_owner = if layer == Layer::Sa { work } else { foreign }; let port_id = Id::new_v4(); let port = make_port( &project, port_id, port_owner, "Work input", PortContract::FunctionInput { incoming_items: vec![], }, ) .unwrap(); project.elements.insert(port_id, port); let mut app = App::with_project(project); let _ = app.update(Message::Diagram(crate::app::Choice { id: diagram, label: app.project().diagrams[&diagram].name.clone(), })); let before = app.document.bundle().clone(); let original = app.project().clone(); let revision = app.document.revision(); send(&mut app, Event::Choose(Some(Tool::Allocation))); assert!( app.connection .as_ref() .unwrap() .candidates .contains(&foreign_performer) ); let context = app.route_context().unwrap(); send(&mut app, Event::Endpoint(context, performer)); assert!(app.connection.as_ref().unwrap().candidates.contains(&work)); assert!(Tool::Allocation.can_connect(app.project(), performer, hidden)); for target in [composite, occupied, foreign, port_id, hidden, performer] { assert!( !app.connection .as_ref() .unwrap() .candidates .contains(&target) ); send(&mut app, Event::Endpoint(context, target)); assert_eq!(app.document.revision(), revision); assert_eq!(app.project(), &original); assert_eq!(app.document.bundle(), &before); } for target in [composite, occupied, foreign, port_id, performer] { assert!( Tool::Allocation .command(app.project(), performer, target) .is_err() ); } send(&mut app, Event::Endpoint(context, work)); assert_eq!( app.document.revision(), revision + 1, "Invalid clicks do not prevent recovery" ); } } #[test] fn allocation_creation_preserves_saved_edge_filters() { let (base, performer, work, diagram) = allocation_fixture(Layer::Oa, ProjectionKind::Decomposition); let mut project = base.project().clone(); project .diagrams .get_mut(&diagram) .unwrap() .view_options .relation_kinds = oa_core::view_options::RelationKinds::Only(vec![]); let diagrams = project.diagrams.clone(); let mut app = App::with_project(project); let _ = app.update(Message::Diagram(crate::app::Choice { id: diagram, label: app.project().diagrams[&diagram].name.clone(), })); let context = start_allocation(&mut app, performer); send(&mut app, Event::Endpoint(context, work)); assert_eq!( app.project().relations[&app.selected].kind(), RelationKind::Allocation ); assert_eq!(app.project().diagrams, diagrams); assert!(project_view(app.project(), diagram).edges.is_empty()); assert!(app.status.contains("view filters are unchanged")); } #[test] fn allocation_mixed_view_keeps_captured_canvas_for_opposite_layer_pairs() { for owner_layer in [Layer::Oa, Layer::Sa] { for app_layer in [Layer::Oa, Layer::Sa] { let (base, owner_performer, owner_work, diagram) = allocation_fixture(owner_layer, ProjectionKind::Traceability); let mut project = base.project().clone(); let layer = if owner_layer == Layer::Oa { Layer::Sa } else { Layer::Oa }; let (performer_kind, work_kind) = if layer == Layer::Oa { (Kind::Actor, Kind::Activity) } else { (Kind::SystemActor, Kind::Function) }; let performer = project.add(performer_kind, "Opposite-layer performer", 40., 60.); let work = project.add(work_kind, "Opposite-layer work", 420., 60.); project.place(diagram, performer, Position { x: 40., y: 350. }); project.place(diagram, work, Position { x: 420., y: 350. }); let mut app = App::with_project(project); let _ = app.update(Message::Diagram(crate::app::Choice { id: diagram, label: app.project().diagrams[&diagram].name.clone(), })); let _ = app.update(Message::Pan(iced::Vector::new(73., 29.))); app.layer = app_layer; let camera = (app.pan, app.zoom); let before = app.document.bundle().clone(); let original = app.project().clone(); let revision = app.document.revision(); let cancelled = start_allocation(&mut app, performer); let _ = app.update(Message::Escape); send(&mut app, Event::Endpoint(cancelled, work)); assert!(app.connection.is_none()); assert_eq!(app.layer, app_layer); assert_eq!(app.active_diagram, Some(diagram)); assert_eq!((app.pan, app.zoom), camera); assert_eq!(app.document.bundle(), &before); let context = start_allocation(&mut app, performer); assert_eq!(app.active_diagram, Some(diagram)); assert_eq!(app.layer, app_layer); assert_eq!(app.route_context(), Some(context)); assert_eq!((app.pan, app.zoom), camera); assert_eq!(app.selected, performer); assert!(app.connection.as_ref().unwrap().candidates.contains(&work)); for wrong_target in [owner_performer, owner_work] { assert!( !app.connection .as_ref() .unwrap() .candidates .contains(&wrong_target) ); send(&mut app, Event::Endpoint(context, wrong_target)); assert_eq!(app.project(), &original); assert_eq!(app.layer, app_layer); assert_eq!(app.active_diagram, Some(diagram)); assert_eq!((app.pan, app.zoom), camera); assert_eq!(app.document.bundle(), &before); } send(&mut app, Event::Endpoint(context, work)); assert!(app.connection.is_none()); assert_eq!(app.document.revision(), revision + 1); assert_eq!(app.active_diagram, Some(diagram)); assert_eq!(app.layer, owner_layer); assert_eq!((app.pan, app.zoom), camera); let relation = &app.project().relations[&app.selected]; assert_eq!( relation.body, RelationBody::new(RelationKind::Allocation, performer, work) ); let mut expected = original.clone(); expected.relations.insert(relation.id, relation.clone()); assert_eq!(app.project(), &expected); let after = app.document.bundle().clone(); let _ = app.update(Message::Undo); assert_eq!(app.project(), &original); assert_eq!(app.document.bundle(), &before); assert_eq!(app.active_diagram, Some(diagram)); assert_eq!((app.pan, app.zoom), camera); let _ = app.update(Message::Redo); assert_eq!(app.project(), &expected); assert_eq!(app.document.bundle(), &after); assert_eq!(app.active_diagram, Some(diagram)); assert_eq!((app.pan, app.zoom), camera); } } } #[test] fn allocation_cancel_navigation_stale_context_and_pending_edits_preserve_model() { let (mut app, performer, work, diagram) = allocation_fixture(Layer::Oa, ProjectionKind::Decomposition); let original = app.project().clone(); let before = app.document.bundle().clone(); let old = start_allocation(&mut app, performer); let _ = app.update(Message::Escape); assert!(app.connection.is_none()); send(&mut app, Event::Endpoint(old, work)); let current = start_allocation(&mut app, performer); send(&mut app, Event::Endpoint(old, work)); assert_eq!(app.connection.as_ref().unwrap().context, current); let _ = app.update(Message::Page(Page::Catalog)); assert!(app.connection.is_none()); let _ = app.update(Message::Page(Page::Diagram)); let _ = app.update(Message::Diagram(crate::app::Choice { id: diagram, label: app.project().diagrams[&diagram].name.clone(), })); start_allocation(&mut app, performer); send(&mut app, Event::Choose(None)); assert!(app.connection.is_none()); start_allocation(&mut app, performer); send(&mut app, Event::Choose(Some(Tool::Interaction))); assert_eq!(app.connection.as_ref().unwrap().tool, Tool::Interaction); assert_eq!(app.project(), &original); let current = start_allocation(&mut app, performer); for stale in [ Context { document: Id::new_v4(), ..current }, Context { diagram: Id::new_v4(), ..current }, Context { revision: current.revision + 1, ..current }, ] { send(&mut app, Event::Endpoint(stale, work)); assert_eq!(app.connection.as_ref().unwrap().context, current); } app.delete_pending = true; send(&mut app, Event::Endpoint(current, work)); assert_eq!(app.project(), &original); let _ = app.update(Message::CancelDelete); let _ = app.update(Message::Escape); let _ = app.update(Message::Select(performer)); let _ = app.update(Message::Field( "/name".into(), "Unapplied performer name".into(), )); send(&mut app, Event::Choose(Some(Tool::Allocation))); assert!(app.connection.is_none()); assert!(app.draft_dirty()); assert_eq!(app.project(), &original); assert_eq!(app.document.bundle(), &before); let (mut source_app, performer, work, _) = allocation_fixture(Layer::Sa, ProjectionKind::Decomposition); let before = source_app.document.bundle().clone(); let context = start_allocation(&mut source_app, performer); use iced::widget::text_editor; let _ = source_app.update(Message::SourceEdit(text_editor::Action::SelectAll)); let _ = source_app.update(Message::SourceEdit(text_editor::Action::Edit( text_editor::Edit::Paste(std::sync::Arc::new("invalid raw source retained".into())), ))); send(&mut source_app, Event::Endpoint(context, work)); send(&mut source_app, Event::Choose(Some(Tool::Allocation))); assert!(source_app.connection.is_none()); assert!(source_app.source_dirty()); assert_eq!( source_app.source_editor.text(), "invalid raw source retained" ); assert_eq!(source_app.document.bundle(), &before); }