use crate::{ app::{App, LoadedSnapshot, Message, Page, Pending}, forms::Draft, workflow::{Event, Workflow}, }; use iced::widget::text_editor; use oa_core::*; use serde_json::json; fn send(app: &mut App, m: Message) { let _ = app.update(m); } fn fixture() -> Project { let mut p = Project::empty(); let owner = p.add(Kind::Entity, "Rescue team", 40., 40.); let work = p.add(Kind::Activity, "Assess incident", 340., 40.); p.relate(RelationKind::Allocation, "Performs", owner, work); p } #[test] fn focused_explorer_follows_reparenting_without_changing_identity() { use crate::explorer::Group; let mut p = Project::empty(); let system = p .add_element(Kind::System, Layer::Sa, "Platform", None) .unwrap(); let machine = p .add_element(Kind::StateMachine, Layer::Sa, "Operating behavior", None) .unwrap(); let mut app = App::with_project(p); send(&mut app, Message::Explore(Some(machine))); assert_eq!(app.explorer_group, Some(Group::Behavior)); send(&mut app, Message::ExplorerCollapseAll); send( &mut app, Message::Command(Command::Reparent { element: machine, parent: Some(system), }), ); assert_eq!( app.project().elements[&machine].parent, Some(system), "{}", app.status ); assert_eq!(app.selected, machine); assert_eq!(app.explorer_focus, Some(machine)); assert_eq!(app.explorer_group, Some(Group::Participants)); assert!(!app.explorer_collapsed.contains(&system)); assert!(!app.explorer_groups_collapsed.contains(&Group::Participants)); send(&mut app, Message::Undo); assert_eq!(app.explorer_group, Some(Group::Behavior)); send(&mut app, Message::Redo); assert_eq!(app.explorer_group, Some(Group::Participants)); } #[test] fn actor_port_creation_updates_containment_and_locate_reveals_its_owner() { let mut project = Project::empty(); let actor = project .add_element(Kind::SystemActor, Layer::Sa, "External operator", None) .unwrap(); let mut app = App::with_project(project.clone()); send(&mut app, Message::Explore(Some(actor))); send( &mut app, Message::PortAction(crate::port_workflow::Action::Component), ); assert!(app.workflow.is_some(), "{}", app.status); send(&mut app, Message::Workflow(Event::Apply)); let port = app.selected; assert_eq!(app.project().elements[&port].parent, Some(actor)); assert!(matches!( port_contract(&app.project().elements[&port]), Some(PortContract::Component { kind: ComponentPortKind::Standard, .. }) )); assert!(app.explorer_index.children(actor).contains(&port)); send(&mut app, Message::ExplorerCollapseAll); send(&mut app, Message::Search("unrelated filter".into())); send(&mut app, Message::ExplorerLocate); assert!(app.search.is_empty()); assert!(!app.explorer_collapsed.contains(&actor)); assert!( !app.explorer_groups_collapsed .contains(&crate::explorer::Group::Participants) ); send(&mut app, Message::AddRelation); let Some(Workflow::Relation(relation)) = &app.workflow else { panic!("boundary workflow") }; assert_eq!(relation.kind, RelationKind::BoundaryExchange); assert_eq!(relation.source.as_ref().map(|c| c.id), Some(port)); send(&mut app, Message::Workflow(Event::Cancel)); send(&mut app, Message::Undo); assert_eq!(app.project(), &project); assert!(app.explorer_index.children(actor).is_empty()); send(&mut app, Message::Redo); assert!(app.explorer_index.children(actor).contains(&port)); } #[test] fn pending_source_prevents_property_draft_collision_and_preserves_both_if_present() { let mut app = App::with_project(Project::empty()); let original = app.project().clone(); send(&mut app, Message::Page(Page::Text)); let source = format!("// Review rationale\n{}", app.source_editor.text()); send( &mut app, Message::SourceEdit(text_editor::Action::SelectAll), ); send( &mut app, Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste( std::sync::Arc::new(source.clone()), ))), ); send(&mut app, Message::Page(Page::Diagram)); send( &mut app, Message::Field( "/name".into(), "Must not enter a hidden property draft".into(), ), ); assert!(!app.draft_dirty()); assert_eq!(app.source_editor.text(), source); assert_eq!(app.project(), &original); // A stale adapter or future integration must not let Apply drop an already // present property draft either. Preserve both and require an explicit choice. app.draft .as_mut() .unwrap() .input("/name", "Retained property draft".into()); assert!(app.draft_dirty()); assert!(!app.apply_source()); assert!(app.source_dirty()); assert!(app.draft_dirty()); assert_eq!(app.source_editor.text(), source); assert_eq!(app.project(), &original); send(&mut app, Message::RevertProperties); assert!(app.apply_source()); send( &mut app, Message::Field("/name".into(), "Reviewed model".into()), ); send(&mut app, Message::Apply); assert_eq!(app.project().name, "Reviewed model"); assert!( app.document.bundle().files[&app.document.bundle().entry] .starts_with("// Review rationale") ); } #[test] fn system_only_models_open_the_populated_perspective() { let mut p = Project::empty(); p.add_element(Kind::Function, Layer::Sa, "Receive report", None) .unwrap(); let diagram = p.ensure_diagram(Layer::Sa); let app = App::with_project(p); assert_eq!(app.layer, Layer::Sa); assert_eq!(app.active_diagram, Some(diagram)); assert_eq!(app.create_kind, Kind::Function); assert_eq!(App::with_project(Project::empty()).layer, Layer::Oa); assert_eq!(App::with_project(Project::demo()).layer, Layer::Oa); } fn source(app: &mut App, text: &str) { send(app, Message::Page(Page::Text)); send(app, Message::SourceEdit(text_editor::Action::SelectAll)); send( app, Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste( std::sync::Arc::new(text.into()), ))), ); } #[test] fn opening_and_creating_models_preserve_the_actual_window_size_for_fit() { let size = iced::Size::new(1100., 700.); let project = fixture(); let mut expected = App::with_project(project.clone()); send(&mut expected, Message::WindowResized(size)); send(&mut expected, Message::Fit); let mut app = App::with_project(Project::empty()); send(&mut app, Message::WindowResized(size)); send( &mut app, Message::Loaded(Ok(Some(LoadedSnapshot::Legacy( std::path::PathBuf::from("resized-model.arcadia"), Box::new(persistence::DecodeResult { project: project.clone(), migration: None, }), std::sync::Arc::from(language::format(&project).into_bytes()), )))), ); assert_eq!(app.window_size, size); assert_eq!(app.zoom, expected.zoom); assert_eq!(app.pan, expected.pan); send(&mut app, Message::Fit); assert_eq!(app.zoom, expected.zoom); send(&mut app, Message::Request(Pending::New)); assert!(app.project().elements.is_empty()); assert_eq!(app.window_size, size); send(&mut app, Message::Request(Pending::Demo)); assert_eq!(app.window_size, size); let mut expected_demo = App::with_project(app.project().clone()); send(&mut expected_demo, Message::WindowResized(size)); send(&mut expected_demo, Message::Fit); assert_eq!(app.zoom, expected_demo.zoom); assert_eq!(app.pan, expected_demo.pan); } #[test] fn every_demo_record_has_a_lossless_graphical_draft() { let p = Project::demo(); for (id, _) in p.identities() { let draft = Draft::from_project(&p, id).expect("Graphical record"); assert!(!draft.dirty()); let candidate = execute_batch(&p, &[draft.command().unwrap()]).unwrap(); assert_eq!(candidate, p, "untouched property form changed {id}"); } } #[test] fn opening_legacy_source_surfaces_report_without_modifying_original_bytes() { let bytes: std::sync::Arc<[u8]> = std::sync::Arc::from( include_bytes!("../../oa-core/tests/fixtures/v2-coastal-rescue.json").as_slice(), ); let decoded = persistence::decode_with_report(&bytes).unwrap(); let original = decoded.project.clone(); let report = decoded.migration.clone().unwrap(); let mut app = App::with_project(Project::empty()); send( &mut app, Message::Loaded(Ok(Some(LoadedSnapshot::Legacy( std::path::PathBuf::from("legacy.json"), Box::new(decoded), bytes.clone(), )))), ); assert_eq!(app.migration, Some(report)); assert_eq!(app.page, Page::Validation); assert_eq!(app.project(), &original); assert!(!app.dirty()); assert!( app.source_snapshot .starts_with(&format!("arcadia {SCHEMA_VERSION};")) ); assert!(serde_json::from_slice::(&bytes).unwrap()["schema_version"] == 2); } #[test] fn fit_uses_measured_canvas_bounds_and_preserves_them_on_open() { let p = fixture(); let mut app = App::with_project(p.clone()); let canvas = iced::Size::new(510., 275.); send(&mut app, Message::CanvasResized(canvas)); send( &mut app, Message::Loaded(Ok(Some(LoadedSnapshot::Legacy( std::path::PathBuf::from("measured.arcadia"), Box::new(persistence::DecodeResult { project: p.clone(), migration: None, }), std::sync::Arc::from(language::format(&p).into_bytes()), )))), ); send(&mut app, Message::Fit); for node in projection::project_view(app.project(), app.active_diagram.unwrap()).nodes { let x = app.pan.x + node.position.x * app.zoom; let y = app.pan.y + node.position.y * app.zoom; assert!(x >= 23.9 && y >= 23.9); assert!(x + node.size.width * app.zoom <= canvas.width - 23.9); assert!(y + node.size.height * app.zoom <= canvas.height - 23.9); } } #[test] fn fit_adapts_to_canvas_returning_after_help_but_preserves_manual_navigation() { let p = fixture(); let mut app = App::with_project(p); send( &mut app, Message::CanvasResized(iced::Size::new(900., 600.)), ); send(&mut app, Message::Page(Page::Guide)); send( &mut app, Message::WindowResized(iced::Size::new(1100., 700.)), ); send(&mut app, Message::Page(Page::Diagram)); let compact = iced::Size::new(500., 270.); send(&mut app, Message::CanvasResized(compact)); let fitted = (app.zoom, app.pan); send(&mut app, Message::Fit); assert_eq!((app.zoom, app.pan), fitted); send(&mut app, Message::Zoom(1.2)); let manual = (app.zoom, app.pan); send( &mut app, Message::CanvasResized(iced::Size::new(650., 400.)), ); assert_eq!((app.zoom, app.pan), manual); } #[test] fn reopening_canvas_for_a_modal_preserves_viewport_after_route_edits() { let mut p = Project::empty(); let sender = p.add(Kind::Activity, "Report", 0., 0.); let target = p.add(Kind::Activity, "Receive", 600., 0.); let edge = p.relate(RelationKind::Interaction, "Report signal", sender, target); let diagram = p.ensure_diagram(Layer::Oa); let mut app = App::with_project(p); let canvas = iced::Size::new(900., 600.); send(&mut app, Message::CanvasResized(canvas)); let viewport = (app.zoom, app.pan); send( &mut app, Message::Command(Command::SetEdgeRoute { diagram, route: EdgeRoute { edge, waypoints: vec![Position { x: 400., y: 400. }], label: Some(EdgeLabel { fraction: 0.5, offset: Position { x: 0., y: 150. }, }), }, }), ); assert_eq!((app.zoom, app.pan), viewport); let mut fitted = App::with_project(app.project().clone()); send(&mut fitted, Message::CanvasResized(canvas)); assert_ne!( (fitted.zoom, fitted.pan), viewport, "The edited route must change the fitted bounds to expose an accidental refit" ); // The shared file modal remounts the canvas sensor. Its on_show measurement // describes the existing viewport, not a user request to fit the edited model. send(&mut app, Message::Export); assert!(app.file_prompt.is_some()); send(&mut app, Message::CanvasResized(canvas)); assert_eq!((app.zoom, app.pan), viewport); send(&mut app, Message::CancelFile); send(&mut app, Message::CanvasResized(canvas)); assert_eq!((app.zoom, app.pan), viewport); let compact = iced::Size::new(650., 400.); send(&mut app, Message::CanvasResized(compact)); send(&mut fitted, Message::CanvasResized(compact)); assert_eq!((app.zoom, app.pan), (fitted.zoom, fitted.pan)); assert_ne!((app.zoom, app.pan), viewport); } #[test] fn source_navigation_waits_for_editor_layout_and_cancels_on_edit() { let p = fixture(); let id = p .elements .values() .find(|e| e.kind() == Kind::Activity) .unwrap() .id; let mut app = App::with_project(p); send(&mut app, Message::Select(id)); send(&mut app, Message::SourceForSelection); assert_eq!(app.page, Page::Text); assert!(app.source_jump.is_some()); let ticket = app.source_reveal_ticket(); send(&mut app, Message::SourceViewport(ticket)); assert!(app.source_jump.is_none()); send(&mut app, Message::SourceForSelection); assert!(app.source_jump.is_some()); send( &mut app, Message::SourceEdit(text_editor::Action::SelectAll), ); assert!(app.source_jump.is_none()); } #[test] fn source_find_navigates_an_invalid_draft_without_applying_or_losing_it() { use crate::source_navigation::Event; let mut app = App::with_project(fixture()); let accepted = app.project().clone(); source(&mut app, "é 救助\nthis is an unfinished model: 救助"); send(&mut app, Message::SourceNavigation(Event::Open(false))); send( &mut app, Message::SourceNavigation(Event::Query("救助".into())), ); send(&mut app, Message::SourceNavigation(Event::Next(false))); let cursor = app.source_jump.unwrap(); assert_eq!( cursor.position, text_editor::Position { line: 0, column: 3 } ); assert_eq!( cursor.selection, Some(text_editor::Position { line: 0, column: 9 }) ); let ticket = app.source_reveal_ticket(); send(&mut app, Message::SourceViewport(ticket)); assert_eq!(app.source_editor.cursor(), cursor); send( &mut app, Message::SourceNavigation(Event::Line("99".into())), ); send(&mut app, Message::SourceNavigation(Event::Go)); assert!(app.status.contains("1 to 2")); assert_eq!(*app.project(), accepted); assert!(app.source_dirty()); } #[test] fn graphical_rename_keeps_aliases_and_references_and_undo() { let mut app = App::with_project(fixture()); let before = app.project().clone(); let id = before .elements .values() .find(|e| e.kind() == Kind::Activity) .unwrap() .id; send(&mut app, Message::Select(id)); send( &mut app, Message::Field("/name".into(), "Coordinate rescue".into()), ); send(&mut app, Message::Apply); assert_eq!( app.project().elements[&id].alias, before.elements[&id].alias ); assert_eq!( crate::source_workspace::compile(&app.source_workspace.bundle()) .unwrap() .decoded .project, *app.project() ); send(&mut app, Message::Undo); assert_eq!(*app.project(), before); } #[test] fn identity_property_change_is_atomic_and_rewrites_all_live_references() { let mut app = App::with_project(fixture()); let before = app.project().clone(); let old = before .elements .values() .find(|e| e.kind() == Kind::Activity) .unwrap() .id; let new = Id::new_v4(); send(&mut app, Message::Select(old)); send(&mut app, Message::Field("/id".into(), new.to_string())); send( &mut app, Message::Field("/name".into(), "New identity".into()), ); send(&mut app, Message::Apply); assert!(!app.project().contains(old)); assert!(app.project().relations.values().any(|r| r.target() == new)); assert!( app.project() .diagrams .values() .any(|d| d.nodes.values().any(|n| n.element == new)) ); send(&mut app, Message::Undo); assert_eq!(*app.project(), before); } #[test] fn invalid_numeric_draft_survives_navigation_save_and_help() { let mut app = App::with_project(fixture()); let p = app.project().clone(); let node = p .diagrams .values() .next() .unwrap() .nodes .values() .next() .unwrap() .id; send(&mut app, Message::Select(node)); send(&mut app, Message::Field("/position/x".into(), "-".into())); send(&mut app, Message::Save(false)); assert!(!app.busy); assert_eq!(*app.project(), p); send(&mut app, Message::Page(Page::Guide)); send(&mut app, Message::Page(Page::Diagram)); assert_eq!(app.page, Page::Diagram); assert_eq!(app.draft.as_ref().unwrap().inputs["/position/x"].raw, "-"); send(&mut app, Message::SelectProject); assert_eq!(app.selected, node); } #[test] fn malformed_source_preserves_model_and_is_recoverable() { let mut app = App::with_project(fixture()); let p = app.project().clone(); source(&mut app, "arcadia 2; ?"); send(&mut app, Message::ApplySource); assert_eq!(*app.project(), p); assert!(!app.source_diagnostics.is_empty()); send(&mut app, Message::Request(Pending::New)); assert!(app.pending.is_some()); send(&mut app, Message::Cancel); assert_eq!(app.source_editor.text(), "arcadia 2; ?"); send(&mut app, Message::RevertSource); assert_eq!( crate::source_workspace::compile(&app.source_workspace.bundle()) .unwrap() .decoded .project, p ); } #[test] fn escaped_string_mode_preserves_exact_control_characters() { let mut app = App::with_project(fixture()); send(&mut app, Message::ExactStrings(true)); let exact = "Plan\r\nRescue\t\"North\""; send( &mut app, Message::Field("/name".into(), serde_json::to_string(exact).unwrap()), ); send(&mut app, Message::Apply); assert_eq!(app.project().name, exact); assert!(!app.draft_dirty()); send( &mut app, Message::Field("/name".into(), "not quoted".into()), ); send(&mut app, Message::Apply); assert_eq!(app.project().name, exact); assert!(app.draft_dirty()); send(&mut app, Message::RevertProperties); assert!(!app.draft_dirty()); } #[test] fn explicit_relationship_builder_never_mutates_on_preview() { let mut p = Project::empty(); let owner = p.add(Kind::Entity, "Team", 0., 0.); let work = p.add(Kind::Activity, "Respond", 300., 0.); let mut workflow = Workflow::relationship(&p, owner, Layer::Oa); let before = p.clone(); workflow .update(&p, Event::RelationName("Performs".into())) .unwrap(); workflow .update( &p, Event::Target(crate::app::Choice { id: work, label: "Respond".into(), }), ) .unwrap(); assert_eq!(p, before); let result = workflow.update(&p, Event::Apply).unwrap().unwrap(); assert_eq!(result.relations.len(), 1); } #[test] fn explorer_and_creation_selection_follow_their_own_placement() { let p = fixture(); let owner = p .elements .values() .find(|e| e.kind() == Kind::Entity) .unwrap() .id; let work = p .elements .values() .find(|e| e.kind() == Kind::Activity) .unwrap() .id; let mut app = App::with_project(p); send(&mut app, Message::Select(owner)); let owner_node = app.selected_node.expect("placed explorer selection"); send(&mut app, Message::Select(work)); let work_node = app.selected_node.expect("placed work selection"); assert_ne!(owner_node, work_node); let before = app.project().clone(); send(&mut app, Message::PlaceSelected); assert_eq!( *app.project(), before, "showing an existing placement does not create a duplicate" ); send(&mut app, Message::CreateKind(Kind::Capability)); send(&mut app, Message::Add(false)); let diagram = &app.project().diagrams[&app.active_diagram.unwrap()]; let new_node = &diagram.nodes[&app.selected_node.expect("new element placement")]; assert_eq!(new_node.element, app.selected); assert_ne!( new_node.id, work_node, "placement actions cannot target previously selected work" ); } #[test] fn bidirectional_transition_ui_commits_once_and_preserves_other_diagrams() { let mut p = Project::empty(); let activity = p.add(Kind::Activity, "Coordinate rescue", 120.5, 85.25); let original = p.clone(); let mut w = Workflow::transition(&p, activity).unwrap(); let next = w.update(&p, Event::Apply).unwrap().unwrap(); assert_eq!( next.diagrams[&original.diagrams.keys().copied().next().unwrap()], *original.diagrams.values().next().unwrap() ); let function = next .elements .values() .find(|e| e.kind() == Kind::Function) .unwrap() .id; let mut reverse = Workflow::transition(&next, function).unwrap(); let again = reverse.update(&next, Event::Apply).unwrap().unwrap(); assert_eq!(again.elements.len(), next.elements.len()); assert_eq!(again.traces.len(), 1); let mut app = App::with_project(original.clone()); send(&mut app, Message::Command(Command::Replace(next))); send(&mut app, Message::Undo); assert_eq!(*app.project(), original); } #[test] fn mission_capability_authoring_is_contextual_atomic_and_text_equivalent() { for (layer, mission_kind, capability_kind, participant_kind) in [ ( Layer::Oa, Kind::OperationalMission, Kind::Capability, Kind::Actor, ), ( Layer::Sa, Kind::Mission, Kind::SystemCapability, Kind::SystemActor, ), ] { let mut p = Project::empty(); let mission = p .add_element(mission_kind, layer, "Protect life", None) .unwrap(); let base = p .add_element(capability_kind, layer, "Rescue people", None) .unwrap(); let extension = p .add_element(capability_kind, layer, "Rescue in heavy weather", None) .unwrap(); let actor = p .add_element(participant_kind, layer, "Rescue operator", None) .unwrap(); let mut app = App::with_project(p); send(&mut app, Message::Select(mission)); send(&mut app, Message::AddRelation); assert!( matches!(app.workflow.as_ref(), Some(Workflow::Relation(d)) if d.kind == RelationKind::MissionCapability && d.source.as_ref().unwrap().id == mission) ); send( &mut app, Message::Workflow(Event::Target(crate::app::Choice { id: base, label: "Rescue people".into(), })), ); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!(app.project().relations.len(), 1); assert!( app.project() .relations .values() .any(|r| r.name == "Protect life exploits Rescue people") ); send(&mut app, Message::Select(mission)); send( &mut app, Message::AddRelationKind(RelationKind::MissionInvolvement, true), ); assert!( matches!(app.workflow.as_ref(), Some(Workflow::Relation(d)) if d.target.as_ref().unwrap().id == mission && d.source.is_none()) ); send( &mut app, Message::Workflow(Event::Source(crate::app::Choice { id: actor, label: "Rescue operator".into(), })), ); send(&mut app, Message::Workflow(Event::Apply)); let before = app.project().clone(); send(&mut app, Message::Select(extension)); send( &mut app, Message::AddRelationKind(RelationKind::CapabilityExtend, false), ); send( &mut app, Message::Workflow(Event::Target(crate::app::Choice { id: base, label: "Rescue people".into(), })), ); let condition = "Sea state >= 6\nAND crew is qualified"; send( &mut app, Message::Workflow(Event::Condition(text_editor::Action::Edit( text_editor::Edit::Paste(std::sync::Arc::new(condition.into())), ))), ); assert_eq!( *app.project(), before, "preview must preserve committed model" ); send(&mut app, Message::Workflow(Event::Apply)); assert!(app.project().relations.values().any(|r| matches!(&r.body, RelationBody::CapabilityExtend {source,target,condition: value} if *source == extension && *target == base && value == condition))); assert_eq!( crate::source_workspace::compile(&app.source_workspace.bundle()) .unwrap() .decoded .project, *app.project() ); send(&mut app, Message::Undo); assert_eq!(*app.project(), before); send(&mut app, Message::Redo); assert_eq!(app.project().relations.len(), 3); } } #[test] fn property_enum_and_optional_reference_roundtrip() { let mut p = Project::empty(); let item = p .add_element(Kind::DataType, Layer::Oa, "Alarm severity", None) .unwrap(); let mut d = Draft::from_project(&p, item).unwrap(); d.shape( "/body/Oa/Shared/DataType/definition", json!({"Enumeration":{"literals":["minor","major"]}}), ) .unwrap(); let p = execute_batch(&p, &[d.command().unwrap()]).unwrap(); assert_eq!(language::parse(&language::format(&p)).unwrap(), p); } #[test] fn modal_owns_background_commands_and_shortcuts() { let mut app = App::with_project(fixture()); send(&mut app, Message::AddRelation); let before = app.project().clone(); send(&mut app, Message::Layer(Layer::Sa)); send(&mut app, Message::Page(Page::Text)); send(&mut app, Message::ShortcutUndo(false)); send(&mut app, Message::Save(false)); assert_eq!(app.layer, Layer::Oa); assert_eq!(app.page, Page::Diagram); assert!(!app.busy); assert_eq!(*app.project(), before); send(&mut app, Message::Escape); assert!(app.workflow.is_none()); } #[test] fn selecting_current_constructors_preserves_payloads() { let mut app = App::with_project(Project::demo()); let before = app.project().clone(); for e in before.elements.values() { send(&mut app, Message::Select(e.id)); send(&mut app, Message::ElementKind(e.kind())); assert!(!app.draft_dirty()); } for r in before.relations.values() { send(&mut app, Message::Select(r.id)); send(&mut app, Message::RelationKind(r.kind())); assert!(!app.draft_dirty()); } let mut p = Project::empty(); let id = p .add_element(Kind::DataType, Layer::Oa, "Severity", None) .unwrap(); let mut app = App::with_project(p); send(&mut app, Message::Select(id)); let path = "/body/Oa/Shared/DataType/definition"; send( &mut app, Message::FieldVariant( path.into(), json!({"Enumeration":{"literals":["minor","major"]}}), ), ); send(&mut app, Message::Apply); let accepted = app.project().clone(); send( &mut app, Message::FieldVariant(path.into(), json!({"Enumeration":{"literals":[]}})), ); send(&mut app, Message::Apply); assert_eq!(*app.project(), accepted); } #[test] fn dragging_keeps_semantic_selection_and_same_node_remains_editable() { let mut app = App::with_project(fixture()); let diagram = app.active_diagram.unwrap(); let placement = app.project().diagrams[&diagram] .nodes .values() .next() .unwrap() .clone(); let semantic = placement.element; send(&mut app, Message::SelectNode(placement.id)); let before = app.project().clone(); for distance in [40., 80.] { send( &mut app, Message::MoveNode( placement.id, Position { x: placement.position.x + distance, y: placement.position.y + distance, }, ), ); assert_eq!(app.selected, semantic); assert_eq!(app.selected_node, Some(placement.id)); assert_eq!( app.draft.as_ref().unwrap().kind, crate::forms::RecordKind::Element ); send(&mut app, Message::SelectNode(placement.id)); assert_eq!(app.selected, semantic); } send(&mut app, Message::Undo); send(&mut app, Message::Undo); assert_eq!(app.project(), &before); // Explicit placement inspection must not make its canvas card unselectable. send(&mut app, Message::Select(placement.id)); send(&mut app, Message::SelectNode(placement.id)); assert_eq!(app.selected, semantic); send( &mut app, Message::Field("/name".into(), "Still editable after dragging".into()), ); send(&mut app, Message::SelectNode(placement.id)); assert_eq!( app.draft.as_ref().unwrap().inputs["/name"].raw, "Still editable after dragging" ); send(&mut app, Message::Apply); assert_eq!( app.project().elements[&semantic].name, "Still editable after dragging" ); } #[test] fn placement_identity_edit_then_drag_and_diagram_identity_then_arrange() { let mut app = App::with_project(fixture()); let diagram = app.active_diagram.unwrap(); let node = *app.project().diagrams[&diagram] .nodes .keys() .next() .unwrap(); send(&mut app, Message::Select(node)); let changed = Id::new_v4(); send(&mut app, Message::Field("/id".into(), changed.to_string())); send(&mut app, Message::SelectNode(node)); send( &mut app, Message::MoveNode(node, Position { x: 200., y: 150. }), ); assert_eq!( app.project().diagrams[&diagram].nodes[&changed].position, Position { x: 200., y: 150. } ); send(&mut app, Message::Select(diagram)); let changed_diagram = Id::new_v4(); send( &mut app, Message::Field("/id".into(), changed_diagram.to_string()), ); send(&mut app, Message::Command(Command::Layout(diagram))); assert_eq!(app.active_diagram, Some(changed_diagram)); assert!(!app.project().contains(diagram)); } #[test] fn placement_relocation_follows_cross_layer_traceability_view() { let mut p = fixture(); let original = p.ensure_diagram(Layer::Oa); let target = p.ensure_diagram(Layer::Sa); p.diagrams.get_mut(&target).unwrap().projection = ProjectionKind::Traceability; let node = *p.diagrams[&original].nodes.keys().next().unwrap(); let mut app = App::with_project(p); send(&mut app, Message::Select(node)); send( &mut app, Message::Field("/diagram".into(), target.to_string()), ); assert!(app.apply(), "{}", app.status); assert_eq!(app.layer, Layer::Sa); assert_eq!(app.active_diagram, Some(target)); assert_eq!(app.selected_node, Some(node)); assert!(app.project().diagrams[&target].nodes.contains_key(&node)); assert!(!app.project().diagrams[&original].nodes.contains_key(&node)); send(&mut app, Message::Select(target)); send(&mut app, Message::Field("/layer".into(), "Oa".into())); assert!(app.apply(), "{}", app.status); assert_eq!(app.layer, Layer::Oa); assert_eq!(app.active_diagram, Some(target)); } #[test] fn manual_trace_requires_target_and_selection_normalizes_creation_kind() { let p = Project::demo(); let a = p .elements .values() .find(|e| e.kind() == Kind::Activity) .unwrap() .id; let b = p .elements .values() .find(|e| e.kind() == Kind::Function) .unwrap() .id; let mut app = App::with_project(p.clone()); send(&mut app, Message::Select(b)); assert_eq!(app.create_kind, Kind::Function); send(&mut app, Message::AddTrace); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!(*app.project(), p); send( &mut app, Message::Workflow(Event::Target(crate::app::Choice { id: a, label: "OA".into(), })), ); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!(app.project().traces.len(), p.traces.len() + 1); send(&mut app, Message::Select(a)); assert_eq!(app.create_kind, Kind::Activity); } #[test] fn invalid_save_and_continue_returns_to_repairable_draft() { let mut app = App::with_project(fixture()); source(&mut app, "arcadia 2; ?"); send(&mut app, Message::Request(Pending::New)); send(&mut app, Message::SaveAndContinue); assert!(app.pending.is_none()); assert_eq!(app.page, Page::Text); send(&mut app, Message::RevertSource); assert!(!app.source_dirty()); } #[test] fn counterpart_creation_focuses_visible_target_and_reuse_keeps_views() { let mut app = App::with_project(fixture()); let source = app .project() .elements .values() .find(|e| e.kind() == Kind::Activity) .unwrap() .id; send(&mut app, Message::Select(source)); send(&mut app, Message::StartTransition); send( &mut app, Message::Workflow(Event::Scope("Selected only".into())), ); send(&mut app, Message::Workflow(Event::Apply)); let target = app.selected; assert_eq!(app.project().elements[&target].kind(), Kind::Function); assert!( app.project().diagrams[&app.active_diagram.unwrap()] .nodes .values() .any(|n| n.element == target) ); let before = app.project().clone(); send(&mut app, Message::StartTransition); send( &mut app, Message::Workflow(Event::Scope("Selected only".into())), ); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!(app.project().diagrams, before.diagrams); assert_eq!(app.selected, source); assert!( app.project().diagrams[&app.active_diagram.unwrap()] .nodes .values() .any(|n| n.element == source) ); } #[test] fn transition_resolutions_belong_to_the_effective_target_and_kind() { use oa_core::transitions::{FieldDecision, TransitionField}; fn transition(workflow: &Workflow) -> &crate::workflow::Transition { let Workflow::Transition(transition) = workflow else { panic!("Transition workflow") }; transition } let mut p = Project::empty(); let source = p .add_element(Kind::Activity, Layer::Oa, "Source work", None) .unwrap(); let first = p .add_element(Kind::Function, Layer::Sa, "First target", None) .unwrap(); let second = p .add_element(Kind::Function, Layer::Sa, "Second target", None) .unwrap(); let trace = Id::new_v4(); p.traces.insert( trace, Trace { id: trace, alias: "work_trace".into(), name: "Work trace".into(), description: String::new(), oa: source, sa: first, kind: TraceKind::Refinement, baseline: None, }, ); let mut workflow = Workflow::transition(&p, source).unwrap(); workflow .update( &p, Event::Resolve( source, TransitionField::Name, FieldDecision::Value("Merged for first target".into()), ), ) .unwrap(); assert!( transition(&workflow).request.mappings.is_empty(), "First target was inferred" ); let choose = |id| crate::app::Choice { id, label: String::new(), }; workflow .update(&p, Event::MappingTarget(source, choose(first))) .unwrap(); assert_eq!( transition(&workflow).request.resolutions.len(), 1, "Same inferred target preserves merged draft" ); workflow .update(&p, Event::MappingTarget(source, choose(second))) .unwrap(); assert!(transition(&workflow).request.resolutions.is_empty()); assert!( transition(&workflow) .plan .conflicts .iter() .any(|c| c.source == Some(source) && c.target == Some(second) && c.field == Some(TransitionField::Name)) ); assert!( workflow.update(&p, Event::Apply).is_err(), "Old resolution cannot overwrite second target" ); workflow .update( &p, Event::Resolve( source, TransitionField::Name, FieldDecision::Value("Reviewed second target".into()), ), ) .unwrap(); workflow .update(&p, Event::MappingTarget(source, choose(second))) .unwrap(); assert_eq!(transition(&workflow).request.resolutions.len(), 1); let accepted = workflow.update(&p, Event::Apply).unwrap().unwrap(); assert_eq!(accepted.elements[&second].name, "Reviewed second target"); assert_eq!(accepted.elements[&first].name, "First target"); workflow .update(&p, Event::MappingTarget(source, choose(Id::nil()))) .unwrap(); assert!( transition(&workflow).request.resolutions.is_empty(), "Creation is a different target context" ); workflow .update( &p, Event::Resolve( source, TransitionField::Description, FieldDecision::Value("Draft for inferred function kind".into()), ), ) .unwrap(); workflow .update(&p, Event::MappingKind(source, Kind::Function)) .unwrap(); assert_eq!( transition(&workflow).request.resolutions.len(), 1, "Same inferred kind preserves draft" ); let participant = p .add_element(Kind::Entity, Layer::Oa, "Participant", None) .unwrap(); let mut kinds = Workflow::transition(&p, participant).unwrap(); kinds .update(&p, Event::MappingKind(participant, Kind::SystemActor)) .unwrap(); kinds .update( &p, Event::Resolve( participant, TransitionField::Description, FieldDecision::Value("Actor review".into()), ), ) .unwrap(); kinds .update(&p, Event::MappingKind(participant, Kind::SystemActor)) .unwrap(); assert_eq!(transition(&kinds).request.resolutions.len(), 1); kinds .update(&p, Event::MappingKind(participant, Kind::System)) .unwrap(); assert!( transition(&kinds).request.resolutions.is_empty(), "Changed kind requires a new decision" ); } #[test] fn behavior_and_data_authoring_do_not_place_nodes_in_unrelated_architecture_views() { let p = Project::demo(); let scenario = p .elements .values() .find(|e| e.layer() == Layer::Sa && e.kind() == Kind::Scenario) .unwrap() .id; let mut app = App::with_project(p); send(&mut app, Message::Layer(Layer::Sa)); send(&mut app, Message::Page(Page::Behavior)); let diagrams = app.project().diagrams.clone(); send(&mut app, Message::Select(scenario)); send(&mut app, Message::CreateKind(Kind::Occurrence)); send(&mut app, Message::Add(true)); let fragment = app.selected; send( &mut app, Message::FieldVariant( "/body/Sa/Shared/Occurrence/body".into(), json!({"Fragment":{"operator":"Optional","branches":[]}}), ), ); assert!(app.apply(), "{}", app.status); send(&mut app, Message::Select(fragment)); send(&mut app, Message::Add(true)); let branch = app.selected; send(&mut app, Message::Select(branch)); send(&mut app, Message::Add(true)); let occurrence = app.selected; assert_eq!(app.project().elements[&fragment].parent, Some(scenario)); assert!( app.project().elements[&scenario] .steps() .unwrap() .contains(&fragment) ); assert!(matches!(app.project().elements[&fragment].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. }) if branches == &vec![branch])); assert_eq!(app.project().elements[&occurrence].parent, Some(branch)); assert_eq!( app.project().elements[&branch].steps().unwrap(), &vec![occurrence] ); assert_eq!(app.project().diagrams, diagrams); send(&mut app, Message::Page(Page::Data)); send(&mut app, Message::CreateKind(Kind::ExchangeItem)); send(&mut app, Message::Add(false)); assert_eq!( app.project().elements[&app.selected].kind(), Kind::ExchangeItem ); assert_eq!(app.project().diagrams, diagrams); send(&mut app, Message::Page(Page::Diagram)); let active = app.active_diagram.unwrap(); send(&mut app, Message::CreateKind(Kind::Function)); send(&mut app, Message::Add(false)); assert_eq!( app.project().diagrams[&active].nodes.len(), diagrams[&active].nodes.len() + 1 ); assert!( app.project().diagrams[&active] .nodes .values() .any(|node| node.element == app.selected) ); for (id, before) in &diagrams { if *id != active { assert_eq!(&app.project().diagrams[id], before); } } } #[test] fn revealing_an_implicit_function_port_unfolds_its_visible_architecture_owner() { let mut p = Project::demo(); let diagram = p .diagrams .values() .find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Allocation) .unwrap() .id; let view = oa_core::projection::project_view(&p, diagram); let function = view .nodes .iter() .find(|n| p.elements[&n.element].kind() == Kind::Function && n.visual_parent.is_some()) .unwrap(); let ancestor = function.visual_parent.unwrap(); let port = p .children(function.element) .find(|e| e.kind() == Kind::Port) .unwrap() .id; p.diagrams .get_mut(&diagram) .unwrap() .nodes .get_mut(&ancestor) .unwrap() .collapsed = true; assert!(crate::navigation::diagrams_containing(&p, port).contains(&diagram)); let mut app = App::with_project(p); send(&mut app, Message::RevealInDiagram(diagram, port)); assert_eq!(app.selected, port); assert_eq!(app.active_diagram, Some(diagram)); assert!(!app.project().diagrams[&diagram].nodes[&ancestor].collapsed); assert!( oa_core::projection::project_view(app.project(), diagram) .ports .iter() .any(|p| p.element == port) ); } #[test] fn creation_from_capabilities_routes_work_to_architecture_and_folded_allocation_inserts_below_existing_work() { let p = Project::demo(); let system = p .elements .values() .find(|e| e.kind() == Kind::System) .unwrap() .id; let capabilities = p .diagrams .values() .find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Capabilities) .unwrap() .id; let mut app = App::with_project(p); send(&mut app, Message::Layer(Layer::Sa)); send( &mut app, Message::Diagram(crate::app::Choice { id: capabilities, label: String::new(), }), ); send(&mut app, Message::CreateKind(Kind::Function)); send(&mut app, Message::Add(false)); let diagram = app.active_diagram.unwrap(); assert_eq!( app.project().diagrams[&diagram].projection, ProjectionKind::Allocation ); assert!( app.project().diagrams[&diagram] .nodes .values() .any(|n| n.element == app.selected) ); send(&mut app, Message::Select(system)); let owner = app.selected_node.unwrap(); send(&mut app, Message::ToggleCollapse); assert!(app.project().diagrams[&diagram].nodes[&owner].collapsed); assert_eq!( app.selected, system, "Folding must retain semantic selection" ); let before = app.project().clone(); send(&mut app, Message::Add(true)); let created = app.selected; let view = oa_core::projection::project_view(app.project(), diagram); let child = view .nodes .iter() .find(|n| n.element == created) .unwrap_or_else(|| panic!("Created work is not visible: {}", app.status)); assert_eq!(child.visual_parent, Some(owner)); assert!(!app.project().diagrams[&diagram].nodes[&owner].collapsed); assert!( view.nodes .iter() .filter(|n| n.visual_parent == Some(owner) && n.id != child.id) .all(|n| n.position.y + n.size.height <= child.position.y) ); let parent = view.nodes.iter().find(|n| n.id == owner).unwrap(); assert!( view.nodes .iter() .filter(|n| n.visual_parent.is_none() && n.id != owner) .all(|n| n.position.x + n.size.width <= parent.position.x || parent.position.x + parent.size.width <= n.position.x || n.position.y + n.size.height <= parent.position.y || parent.position.y + parent.size.height <= n.position.y) ); send(&mut app, Message::Undo); assert_eq!(app.project(), &before); } #[test] fn nested_allocation_insertion_reflows_sibling_subtrees_and_preserves_fold_state() { use oa_core::projection::{project_diagram, project_view}; for folded in [false, true] { let mut p = Project::empty(); let root = p .add_element(Kind::Entity, Layer::Oa, "Root", None) .unwrap(); let a = p .add_element(Kind::Entity, Layer::Oa, "A", Some(root)) .unwrap(); let b = p .add_element(Kind::Entity, Layer::Oa, "B", Some(root)) .unwrap(); let other = p .add_element(Kind::Entity, Layer::Oa, "Other root", None) .unwrap(); let a_work = p .add_element(Kind::Activity, Layer::Oa, "A work", None) .unwrap(); let b_work = p .add_element(Kind::Activity, Layer::Oa, "B work", None) .unwrap(); // A preserved OA boundary remains explicit Legacy content in schema3. let b_port = Id::new_v4(); p.elements.insert( b_port, oa_core::Element { id: b_port, alias: "b_boundary".into(), name: "B boundary".into(), description: String::new(), parent: Some(b), body: ElementBody::shared_in( Layer::Oa, SharedBody::Port { contract: PortContract::Legacy, legacy: Some(LegacyPortContract { direction: PortDirection::InOut, interface: None, }), }, ), }, ); p.relate(RelationKind::Allocation, "A performs", a, a_work); p.relate(RelationKind::Allocation, "B performs", b, b_work); let diagram = p.ensure_diagram(Layer::Oa); p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation; for id in [root, a, b, other, a_work, b_work] { p.place(diagram, id, Position { x: 60., y: 60. }); } let mut p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap(); let view = project_view(&p, diagram); let root_node = view.nodes.iter().find(|n| n.element == root).unwrap(); let a_node = view.nodes.iter().find(|n| n.element == a).unwrap(); let b_node = view.nodes.iter().find(|n| n.element == b).unwrap(); // Layout's UUID order is intentionally irrelevant to this scenario: // explicitly put A before B, as a user can arrange these participants. p = execute_batch( &p, &[ Command::MoveNode { diagram, node: a_node.id, position: Position { x: root_node.position.x + 24., y: root_node.position.y + 64., }, with_descendants: true, }, Command::MoveNode { diagram, node: b_node.id, position: Position { x: root_node.position.x + 24., y: root_node.position.y + 64. + a_node.size.height + 16., }, with_descendants: true, }, ], ) .unwrap(); let view = project_view(&p, diagram); let root_node = view.nodes.iter().find(|n| n.element == root).unwrap(); let other_node = view.nodes.iter().find(|n| n.element == other).unwrap().id; let b_node = view.nodes.iter().find(|n| n.element == b).unwrap().id; let b_work_node = view.nodes.iter().find(|n| n.element == b_work).unwrap().id; p.diagrams .get_mut(&diagram) .unwrap() .nodes .get_mut(&other_node) .unwrap() .position = Position { x: root_node.position.x, y: root_node.position.y + root_node.size.height + 24., }; p.diagrams .get_mut(&diagram) .unwrap() .nodes .get_mut(&b_node) .unwrap() .collapsed = folded; // Deliberately overlapping unrelated roots must not be rearranged. let mut unrelated = Vec::new(); for name in ["Unrelated one", "Unrelated two"] { let id = p.add_element(Kind::Entity, Layer::Oa, name, None).unwrap(); unrelated.push(p.place(diagram, id, Position { x: 2000., y: 1000. })); } let before = p.clone(); let mut expanded = p.diagrams[&diagram].clone(); expanded.nodes.get_mut(&b_node).unwrap().collapsed = false; let before_view = project_diagram(&p, &expanded); let before_port = before_view .ports .iter() .find(|p| p.element == b_port) .unwrap() .position; let mut app = App::with_project(p); send(&mut app, Message::Select(a)); send(&mut app, Message::CreateKind(Kind::Activity)); send(&mut app, Message::Add(true)); let created = app.selected; assert_eq!( app.project().elements[&created].kind(), Kind::Activity, "{}", app.status ); let after = &app.project().diagrams[&diagram]; assert_eq!(after.nodes[&b_node].collapsed, folded); let dy = after.nodes[&b_node].position.y - before.diagrams[&diagram].nodes[&b_node].position.y; assert!(dy > 0., "B must move below the enlarged A"); assert_eq!( after.nodes[&b_work_node].position.y, before.diagrams[&diagram].nodes[&b_work_node].position.y + dy ); assert!( after.nodes[&other_node].position.y > before.diagrams[&diagram].nodes[&other_node].position.y ); for id in unrelated { assert_eq!(after.nodes[&id], before.diagrams[&diagram].nodes[&id]); } let mut expanded = after.clone(); expanded.nodes.get_mut(&b_node).unwrap().collapsed = false; let view = project_diagram(app.project(), &expanded); let after_port = view .ports .iter() .find(|p| p.element == b_port) .unwrap() .position; assert_eq!( after_port, Position { x: before_port.x, y: before_port.y + dy } ); for (left, right) in [(a, b), (root, other), (a_work, created)] { let left = view.nodes.iter().find(|n| n.element == left).unwrap(); let right = view.nodes.iter().find(|n| n.element == right).unwrap(); assert!(left.position.y + left.size.height <= right.position.y); } assert!(crate::diagram::insertion_adjustments(app.project(), diagram, created).is_empty()); send(&mut app, Message::Undo); assert_eq!( app.project(), &before, "Creation and all reflow must undo together" ); if folded { send(&mut app, Message::Select(root)); send(&mut app, Message::CreateKind(Kind::Entity)); send(&mut app, Message::Add(true)); let mut expanded = app.project().diagrams[&diagram].clone(); expanded.nodes.get_mut(&b_node).unwrap().collapsed = false; let view = project_diagram(app.project(), &expanded); let b = view.nodes.iter().find(|n| n.id == b_node).unwrap(); let created = view .nodes .iter() .find(|n| n.element == app.selected) .unwrap(); assert!(b.position.y + b.size.height <= created.position.y); assert!(app.project().diagrams[&diagram].nodes[&b_node].collapsed); } } } #[test] fn routing_actions_are_atomic_guarded_and_fit_the_authored_detour() { use crate::route_edit::Event as Route; let mut p = Project::empty(); let sender = p.add(Kind::Activity, "Report", 0., 0.); let target = p.add(Kind::Activity, "Receive", 600., 0.); let edge = p.relate(RelationKind::Interaction, "Report signal", sender, target); let diagram = p.ensure_diagram(Layer::Oa); let mut app = App::with_project(p.clone()); send(&mut app, Message::Select(edge)); send(&mut app, Message::Route(Route::AddBend)); assert_eq!( app.project().diagrams[&diagram].routes[0].waypoints.len(), 1 ); let context = app.route_context().unwrap(); let mut route = app.project().diagrams[&diagram].routes[0].clone(); route.waypoints[0] = Position { x: 400., y: -700. }; send( &mut app, Message::Route(Route::Commit(context, route.clone(), Some(0))), ); assert_eq!(app.project().diagrams[&diagram].routes[0], route); send(&mut app, Message::Fit); let pin = route.waypoints[0]; assert!(pin.y * app.zoom + app.pan.y >= 0.); send(&mut app, Message::Route(Route::RemoveBend)); assert!( app.project().diagrams[&diagram].routes[0] .waypoints .is_empty() ); send(&mut app, Message::Undo); assert_eq!(app.project().diagrams[&diagram].routes[0], route); let accepted = app.project().clone(); send( &mut app, Message::Route(Route::Commit(context, route.clone(), Some(0))), ); assert_eq!( app.project(), &accepted, "A stale gesture must not overwrite newer routing" ); let current = app.route_context().unwrap(); send(&mut app, Message::Escape); send( &mut app, Message::Route(Route::Commit(current, route.clone(), Some(0))), ); assert_eq!( app.project(), &accepted, "Escape invalidates the gesture epoch" ); send(&mut app, Message::Route(Route::Reset)); assert!(app.project().diagrams[&diagram].routes.is_empty()); send(&mut app, Message::Undo); assert_eq!(app.project(), &accepted); let current = app.route_context().unwrap(); source(&mut app, "invalid unsaved source"); send( &mut app, Message::Route(Route::Commit(current, route, Some(0))), ); assert_eq!(app.project(), &accepted); assert_eq!(app.source_editor.text(), "invalid unsaved source"); } #[test] fn adding_bend_between_close_ports_stays_outside_owner_clearance() { let mut p = Project::empty(); let diagram = p.ensure_diagram(Layer::Sa); let a = p .add_element(Kind::Function, Layer::Sa, "Send", None) .unwrap(); let b = p .add_element(Kind::Function, Layer::Sa, "Receive", None) .unwrap(); let edge = p.relate(RelationKind::FunctionalExchange, "Status", a, b); p.place(diagram, a, Position { x: 0., y: 100. }); p.place(diagram, b, Position { x: 260., y: 100. }); let mut app = App::with_project(p); send(&mut app, Message::Select(edge)); send(&mut app, Message::Route(crate::route_edit::Event::AddBend)); let p = app.project(); let view = projection::project_view(p, diagram); let routed = routing::route_diagram(p, &p.diagrams[&diagram], &view); assert!( routed.edges[&edge].warnings.is_empty(), "{:?}", routed.edges[&edge].warnings ); assert_eq!( p.diagrams[&diagram].routes[0].waypoints, vec![Position { x: 242., y: view .ports .iter() .find(|port| p.elements[&port.element].parent == Some(a)) .unwrap() .position .y }] ); }