use super::*; use crate::behavior::scenario_layout::Diagnostic; use crate::document::Document; use oa_core::{ Diagram, DiagramLayout, ProjectionKind, ScenarioLayout, language, source_project::ProjectLimits, }; fn id(project: &Project, alias: &str) -> Id { project .identities() .into_iter() .find(|(_, name)| *name == alias) .unwrap() .0 } fn fixture(layout: Option) -> (Document, Id, Id) { let mut project = language::parse(FIXTURE).unwrap(); let owner = id(&project, "conversation"); let mut add = |alias: &str, layout: Option| { let diagram = Id::new_v4(); project.diagrams.insert( diagram, Diagram { id: diagram, alias: alias.into(), name: alias.into(), description: String::new(), layer: oa_core::Layer::Oa, projection: ProjectionKind::Scenario { scenario: owner }, nodes: Default::default(), routes: vec![], view_options: Default::default(), layout: layout.map(DiagramLayout::Scenario), }, ); diagram }; let a = add("saved_a", layout); let b = add("saved_b", Some(ScenarioLayout::default())); let document = Document::from_project(project, ProjectLimits::default()).unwrap(); (document, a, b) } fn resolved(project: &Project, diagram: Id, selected: Id) -> ResolvedScenarioPlan { resolve(project, Representation::Saved { diagram }, selected).unwrap() } fn same_geometry(a: &ResolvedScenarioPlan, b: &ResolvedScenarioPlan) { assert_eq!(a.bounds, b.bounds); assert_eq!(a.lanes, b.lanes); assert_eq!(a.plan.frames, b.plan.frames); assert_eq!(a.plan.scopes, b.plan.scopes); let (a, b) = (&a.scene, &b.scene); assert_eq!(a.rows, b.rows); assert_eq!(a.sequence_bounds, b.sequence_bounds); assert_eq!(a.size, b.size); assert_eq!(a.layout_origin, b.layout_origin); assert_eq!(a.lanes, b.lanes); assert_eq!(a.render_lanes, b.render_lanes); assert_eq!(a.activations, b.activations); assert_eq!(a.lifecycle, b.lifecycle); assert_eq!(a.open_calls, b.open_calls); let slots = |scene: &crate::behavior::Scene| { scene .slots .iter() .map(|item| { ( item.slot.parent, item.slot.before, item.y, item.label.clone(), ) }) .collect::>() }; assert_eq!(slots(a), slots(b)); let lines = |scene: &crate::behavior::Scene| { scene .lines .iter() .map(|line| { ( line.id, line.points.clone(), line.label_at, line.label.clone(), line.dashed, ) }) .collect::>() }; let boxes = |scene: &crate::behavior::Scene| { scene .boxes .iter() .map(|item| { ( item.id, item.rect, item.title.clone(), item.detail.clone(), item.marker, ) }) .collect::>() }; assert_eq!(lines(a), lines(b)); assert_eq!(boxes(a), boxes(b)); } fn expected_message(project: &Project, diagram: Id, occurrence: Id, y: f32) -> Project { // Independent expected mutation: tests begin with no message overrides. let mut expected = project.clone(); expected.diagrams.get_mut(&diagram).unwrap().layout = Some(DiagramLayout::Scenario(ScenarioLayout { lanes: vec![], messages: vec![ScenarioMessagePosition { address: OccurrenceEndpoint { references: vec![], occurrence, }, send_y: y, }], })); expected } #[test] fn one_repeated_message_moves_in_one_view_with_exact_document_undo_redo_and_reopen() { let (mut document, a, b) = fixture(None); let original = document.snapshot(); let before = document.project().clone(); let first = id(&before, "first"); let second = id(&before, "second"); let baseline = resolved(&before, a, second); assert_eq!(baseline.scene.rows[&first].1, 148.); assert_eq!(baseline.scene.rows[&second].1, 240.); let untouched = resolved(&before, b, second); let drag = capture(&before, a, Target::Message(second)).unwrap(); let preview = drag.prepare(-40.).unwrap().unwrap(); let expected = expected_message(&before, a, second, 200.); assert_eq!(preview.project, expected); assert_eq!(preview.geometry.scene.rows[&first].1, 148.); assert_eq!(preview.geometry.scene.rows[&second].1, 200.); let line = preview .geometry .scene .lines .iter() .find(|line| line.id == second) .unwrap(); assert_eq!(line.points.first().unwrap().y, 200.); assert_eq!(line.points.last().unwrap().y, 200.); assert_eq!( preview.geometry.scene.activations, baseline.scene.activations ); assert_eq!(preview.geometry.scene.lifecycle, baseline.scene.lifecycle); same_geometry(&resolved(&preview.project, b, second), &untouched); assert_eq!(drag.original, before); assert!(std::sync::Arc::ptr_eq(&original, &document.snapshot())); assert!(!document.can_undo()); let returned_selection = document.execute(preview.command).unwrap(); assert_eq!( returned_selection, Some(a), "controller must preserve the dragged semantic selection separately" ); assert_eq!(document.project(), &expected); assert_eq!(document.revision(), 1); let accepted = document.bundle().clone(); same_geometry(&resolved(document.project(), a, second), &preview.geometry); assert!(document.undo()); assert_eq!(document.project(), &before); assert_eq!(document.bundle(), &original.bundle); assert!(!document.can_undo()); assert!(document.redo()); assert_eq!(document.project(), &expected); assert_eq!(document.bundle(), &accepted); let reopened = Document::from_bundle(accepted, ProjectLimits::default()).unwrap(); assert_eq!(reopened.project(), &expected); same_geometry(&resolved(reopened.project(), a, second), &preview.geometry); assert!(!reopened.dirty() && !reopened.can_undo()); } #[test] fn horizontal_lane_move_changes_actual_header_and_attached_endpoints_only_in_selected_view() { let (document, a, b) = fixture(None); let before = document.project(); let right = id(before, "right"); let first = id(before, "first"); let other = resolved(before, b, right); let preview = capture(before, a, Target::Lane(right)) .unwrap() .prepare(100.) .unwrap() .unwrap(); let mut expected = before.clone(); expected.diagrams.get_mut(&a).unwrap().layout = Some(DiagramLayout::Scenario(ScenarioLayout { lanes: vec![ScenarioLanePosition { lifeline: right, x: 500., }], messages: vec![], })); assert_eq!(preview.project, expected); assert_eq!(preview.geometry.scene.lanes[&right], 500.); let header = preview .geometry .scene .boxes .iter() .find(|item| item.id == right) .unwrap(); assert_eq!(header.rect.center_x(), 500.); let line = preview .geometry .scene .lines .iter() .find(|line| line.id == first) .unwrap(); assert_eq!(line.points.first().unwrap().x, 160.); assert_eq!(line.points.last().unwrap().x, 500.); assert!(line.points.iter().all(|point| point.y == 148.)); same_geometry(&resolved(&preview.project, b, right), &other); assert_eq!(document.project(), before); assert!(!document.dirty() && !document.can_undo()); } #[test] fn drawn_unassigned_lifeline_can_move_without_assigning_a_semantic_participant() { let (document, a, _) = fixture(None); let mut project = document.project().clone(); let owner = id(&project, "conversation"); let lane = project .add_element( Kind::Lifeline, oa_core::Layer::Oa, "Unassigned header", Some(owner), ) .unwrap(); let document = Document::from_project(project, ProjectLimits::default()).unwrap(); let baseline = resolved(document.project(), a, lane); assert!(!baseline.scene.lanes.contains_key(&lane)); let before_header = baseline .scene .boxes .iter() .find(|item| item.id == lane) .unwrap() .rect; let preview = capture(document.project(), a, Target::Lane(lane)) .unwrap() .prepare(70.) .unwrap() .unwrap(); let after_header = preview .geometry .scene .boxes .iter() .find(|item| item.id == lane) .unwrap() .rect; assert_eq!(after_header.x, before_header.x + 70.); assert_eq!(after_header.y, before_header.y); assert_eq!(after_header.size(), before_header.size()); assert_eq!(preview.project.elements, document.project().elements); assert_eq!(preview.project.relations, document.project().relations); assert!(!document.can_undo()); } #[test] fn self_call_spacing_uses_send_y_and_preserves_reply_and_actual_activation_finish() { let (document, a, _) = fixture(None); let before = document.project(); let call = id(before, "call"); let reply = id(before, "reply"); let baseline = resolved(before, a, call); let preview = capture(before, a, Target::Message(call)) .unwrap() .prepare(-22.) .unwrap() .unwrap(); assert_eq!(preview.project, expected_message(before, a, call, 310.)); let line = preview .geometry .scene .lines .iter() .find(|line| line.id == call) .unwrap(); assert_eq!(line.points.first().unwrap().y, 310.); assert_eq!(line.points.last().unwrap().y, 334.); assert_eq!(preview.geometry.scene.rows[&reply].1, 424.); assert_eq!(preview.geometry.scene.activations.len(), 1); let mut expected_activation = baseline.scene.activations.clone(); expected_activation[0].rect.y = 334.; expected_activation[0].rect.height = 90.; assert_eq!(preview.geometry.scene.activations, expected_activation); assert_eq!(preview.geometry.scene.lifecycle, baseline.scene.lifecycle); } #[test] fn feasible_spacing_crossing_remains_authored_with_diagnostic_and_unchanged_steps() { let (document, a, _) = fixture(None); let before = document.project(); let first = id(before, "first"); let second = id(before, "second"); let owner = id(before, "conversation"); let preview = capture(before, a, Target::Message(second)) .unwrap() .prepare(-120.) .unwrap() .unwrap(); assert_eq!(preview.project, expected_message(before, a, second, 120.)); assert_eq!(preview.geometry.scene.rows[&second].1, 120.); assert_eq!(preview.geometry.scene.rows[&first].1, 148.); assert_eq!( preview.project.elements[&owner].steps(), before.elements[&owner].steps() ); let shown = crate::behavior::scenario_layout::presentation::present( &preview.project, Representation::Saved { diagram: a }, second, ); assert!(shown.capabilities().coordinate_input); assert!( shown .diagnostics() .contains(&Diagnostic::SourceOrderCrossing { scope: owner, before: first, after: second }) ); } #[test] fn unsupported_self_call_move_is_refused_and_automatic_fallback_is_not_editable_capture() { let (document, a, _) = fixture(None); let before = document.project(); let call = id(before, "call"); let drag = capture(before, a, Target::Message(call)).unwrap(); let Err(Refusal::Geometry(issues)) = drag.prepare(118.) else { panic!("send450 / receive474 after reply424 must be refused") }; assert!(issues.iter().any(|issue| matches!(issue, GeometryIssue::TypedActivationCrossingNotSupported { call: id, .. } if *id == call))); assert_eq!(&drag.original, before); let invalid_geometry = expected_message(before, a, call, 450.); let shown = crate::behavior::scenario_layout::presentation::present( &invalid_geometry, Representation::Saved { diagram: a }, call, ); assert!(shown.capabilities().geometry_navigation && !shown.capabilities().coordinate_input); assert!(matches!( capture(&invalid_geometry, a, Target::Message(call)), Err(Refusal::Geometry(_)) )); assert!(!document.dirty() && !document.can_undo()); } #[test] fn invalid_view_foreign_identity_and_latent_message_are_not_spacing_targets() { let (document, a, b) = fixture(None); let p = document.project(); let unfinished = id(p, "unfinished"); let source = id(p, "source"); assert!(matches!( capture(p, Id::nil(), Target::Lane(source)), Err(Refusal::InvalidSavedView(_)) )); for target in [ Target::Lane(source), Target::Lane(b), Target::Message(source), Target::Message(unfinished), ] { assert!( matches!(capture(p, a, target), Err(Refusal::TargetNotRendered(found)) if found == target) ); } // A stored latent coordinate is not permission to fabricate a painted arrow. let mut latent = expected_message(p, a, unfinished, 2000.); let foreign_owner = latent .add_element(Kind::Scenario, oa_core::Layer::Oa, "Foreign", None) .unwrap(); let foreign_lane = latent .add_element( Kind::Lifeline, oa_core::Layer::Oa, "Foreign lane", Some(foreign_owner), ) .unwrap(); *latent .elements .get_mut(&foreign_lane) .unwrap() .shared_mut() .unwrap() = SharedBody::Lifeline { represented: Some(source), }; let latent_document = Document::from_project(latent, ProjectLimits::default()).unwrap(); assert!(matches!( capture(latent_document.project(), a, Target::Message(unfinished)), Err(Refusal::TargetNotRendered(_)) )); assert!(matches!( capture(latent_document.project(), a, Target::Lane(foreign_lane)), Err(Refusal::TargetNotRendered(_)) )); } #[test] fn zero_motion_return_to_start_and_subprecision_preserve_sparse_intent_and_real_redo() { let parsed = language::parse(FIXTURE).unwrap(); let left = id(&parsed, "left"); let second = id(&parsed, "second"); let layouts = [ None, Some(ScenarioLayout::default()), Some(ScenarioLayout { lanes: vec![], messages: vec![ScenarioMessagePosition { address: OccurrenceEndpoint { references: vec![], occurrence: second, }, send_y: 240., }], }), Some(ScenarioLayout { lanes: vec![ScenarioLanePosition { lifeline: left, x: -0., }], messages: vec![], }), ]; for (index, layout) in layouts.into_iter().enumerate() { let (mut document, a, _) = fixture(layout); let owner = id(document.project(), "conversation"); document .execute(Command::Rename(owner, "A real redo entry".into())) .unwrap(); let redo = document.snapshot(); assert!(document.undo()); let before = document.snapshot(); let revision = document.revision(); let target = if index == 3 { Target::Lane(left) } else { Target::Message(second) }; let drag = capture(document.project(), a, target).unwrap(); if index == 3 { let Some(DiagramLayout::Scenario(layout)) = &drag.original.diagrams[&a].layout else { panic!() }; assert_eq!(layout.lanes[0].x.to_bits(), (-0_f32).to_bits()); } assert!(drag.prepare(10.).unwrap().is_some()); for delta in [0., -0.] { assert!(drag.prepare(delta).unwrap().is_none()); } if index != 3 { assert!(drag.prepare(f32::from_bits(1)).unwrap().is_none()); } assert_eq!( serde_json::to_vec(&drag.original).unwrap(), serde_json::to_vec(document.project()).unwrap() ); assert!(std::sync::Arc::ptr_eq(&before, &document.snapshot())); assert_eq!(document.bundle(), &before.bundle); assert_eq!(document.revision(), revision); assert!(document.can_redo()); assert!(document.redo()); assert_eq!(document.project(), &redo.decoded.project); assert_eq!(document.bundle(), &redo.bundle); } } #[test] fn malformed_or_out_of_range_motion_cannot_mutate_original() { let (document, a, _) = fixture(None); let drag = capture( document.project(), a, Target::Message(id(document.project(), "second")), ) .unwrap(); for delta in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { assert!(matches!(drag.prepare(delta), Err(Refusal::InvalidDelta))); } assert!(matches!(drag.prepare(f32::MAX), Err(Refusal::Command(_)))); assert_eq!(&drag.original, document.project()); assert!(!document.dirty() && !document.can_undo()); } const FIXTURE: &str = r#"arcadia 7; project spacing_probe "Saved spacing probe" id "71000000-0000-4000-8000-000000000001" { oa_activity source "Source" id "71000000-0000-4000-8000-000000000002" {} oa_activity target "Target" id "71000000-0000-4000-8000-000000000003" {} interaction exchange "Repeated exchange" id "71000000-0000-4000-8000-000000000004" { source @source; target @target; } oa_scenario conversation "Conversation" id "71000000-0000-4000-8000-000000000005" { viewpoint OperationalActivity; steps [@first, @second, @call, @reply, @unfinished]; } oa_lifeline left "Left" id "71000000-0000-4000-8000-000000000006" { parent @conversation; represented @source; } oa_lifeline right "Right" id "71000000-0000-4000-8000-000000000007" { parent @conversation; represented @target; } oa_occurrence first "First" id "71000000-0000-4000-8000-000000000008" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; } oa_occurrence second "Second" id "71000000-0000-4000-8000-000000000009" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; } oa_occurrence call "Call" id "71000000-0000-4000-8000-00000000000a" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Call; }; }; } oa_occurrence reply "Reply" id "71000000-0000-4000-8000-00000000000b" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Reply; reply_to @call; }; }; } oa_occurrence unfinished "Unfinished" id "71000000-0000-4000-8000-00000000000c" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver none; message_kind Asynchronous; }; }; } } "#;