//! Transition navigation must not add presentation edits to a behavior review. use super::*; use crate::workflow::{Event, Workflow}; use oa_core::transitions::*; fn send(app: &mut App, message: Message) { let _ = app.update(message); } fn element( p: &mut Project, number: u128, kind: Kind, layer: Layer, name: &str, parent: Option, ) -> Id { let mut e = p.make_element(kind, layer, name, parent).unwrap(); e.id = Id::from_u128(number); let id = e.id; p.elements.insert(id, e); id } fn fixture(layer: Layer, kind: Kind) -> (App, Id, Id, Id) { let mut p = Project::empty(); let other = if layer == Layer::Oa { Layer::Sa } else { Layer::Oa }; let work_kind = |l| { if l == Layer::Oa { Kind::Activity } else { Kind::Function } }; let work = element(&mut p, 1, work_kind(layer), layer, "Assess pressure", None); let reused = element(&mut p, 10, work_kind(other), other, "Assess pressure", None); let root = element(&mut p, 100, kind, layer, "Restore service", None); if kind == Kind::Scenario { let line = element(&mut p, 2, Kind::Lifeline, layer, "Assessment", Some(root)); *p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(work), }; } let view = p.ensure_diagram(other); p.diagrams.get_mut(&view).unwrap().name = "Unrelated architecture".into(); p.place(view, reused, Position { x: 873., y: 421. }); let mut app = App::with_project(p); app.layer = layer; app.page = Page::Behavior; app.selected = root; app.behavior.active_owner = Some(root); app.refresh_draft(); (app, root, work, reused) } fn review( app: &mut App, selected: Id, mapping: Option<(Id, Id)>, destination: Option, ) -> (Id, Project) { app.selected = selected; app.refresh_draft(); send(app, Message::StartTransition); let Some(Workflow::Transition(t)) = &mut app.workflow else { panic!("{}", app.status) }; t.request.scope = DependencyScope::Subtree; t.request.destination_diagram = destination; if let Some((source, target)) = mapping { t.request.mappings.push(TransitionMapping { source, target: Some(target), target_kind: None, }); } t.plan = plan_transition(app.document.project(), &t.request).unwrap(); assert!(t.plan.conflicts.is_empty(), "{:?}", t.plan.conflicts); let target = t .plan .changes .iter() .find(|c| c.source == selected) .unwrap() .target; let expected = apply_transition(app.document.project(), &t.plan).unwrap(); (target, expected) } #[test] fn scenario_transition_focuses_requested_counterpart_and_preserves_unrelated_views_in_both_directions() { for layer in [Layer::Oa, Layer::Sa] { let (mut app, root, work, reused) = fixture(layer, Kind::Scenario); let stale_selection = crate::behavior::scenario::CanvasEvent::Select( app.scenario_context().unwrap(), Id::from_u128(2), ); let before = app.document.snapshot(); let (target, expected) = review(&mut app, root, Some((work, reused)), None); let Some(Workflow::Transition(t)) = &app.workflow else { unreachable!() }; assert_ne!( t.plan.changes.first().unwrap().source, root, "Fixture must expose a dependency before the selected root" ); app.selected_node = Some(Id::from_u128(999)); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!( app.project(), &expected, "Desktop must preserve the core candidate exactly" ); assert_eq!(app.project().diagrams, before.decoded.project.diagrams); assert_eq!(app.page, Page::Behavior); assert_eq!(app.selected, target); assert_eq!(app.behavior.active_owner, Some(target)); assert_eq!(app.scenario_context().unwrap().scenario, target); assert_eq!(app.selected_node, None); assert!(app.workflow.is_none()); assert_eq!(app.document.revision(), 1); let accepted = app.document.snapshot(); send(&mut app, Message::ScenarioCanvas(stale_selection.clone())); assert_eq!(app.selected, target); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert_eq!( source_project::compile_bundle(&accepted.bundle, LIMITS) .unwrap() .decoded .project, expected ); send(&mut app, Message::Undo); assert_eq!(app.project(), &before.decoded.project); assert_eq!(app.document.bundle(), &before.bundle); assert!(!app.document.can_undo()); let undone_selection = app.selected; send(&mut app, Message::ScenarioCanvas(stale_selection)); assert_eq!(app.selected, undone_selection); assert_eq!(app.project(), &before.decoded.project); send(&mut app, Message::Redo); assert_eq!(app.project(), &accepted.decoded.project); assert_eq!(app.document.bundle(), &accepted.bundle); } } #[test] fn explicit_behavior_internal_and_process_roots_keep_their_workspace_without_creating_a_view() { for (layer, kind) in [ (Layer::Oa, Kind::Scenario), (Layer::Sa, Kind::Scenario), (Layer::Oa, Kind::Process), (Layer::Sa, Kind::FunctionalChain), ] { let (mut app, root, work, reused) = fixture(layer, kind); let mut p = app.project().clone(); p.diagrams.clear(); app = App::with_project(p); app.page = Page::Behavior; let selected = if kind == Kind::Scenario { Id::from_u128(2) } else { root }; let (target, expected) = review(&mut app, selected, Some((work, reused)), None); let owner = crate::behavior::selected_behavior(&expected, target).unwrap(); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!(app.project(), &expected); assert!(app.project().diagrams.is_empty()); assert_eq!(app.selected, target); assert_eq!(app.behavior.active_owner, Some(owner)); assert_eq!(app.page, Page::Behavior); } } #[test] fn explicit_destination_is_honored_and_ordinary_work_transition_still_places_in_architecture() { let (mut app, root, work, reused) = fixture(Layer::Oa, Kind::Scenario); let destination = *app.project().diagrams.keys().next().unwrap(); let (target, expected) = review(&mut app, root, Some((work, reused)), Some(destination)); assert!( expected.diagrams[&destination] .nodes .values() .any(|n| n.element == target) ); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!(app.project(), &expected); assert_eq!(app.page, Page::Behavior); let (mut app, _, work, _) = fixture(Layer::Oa, Kind::Scenario); let (target, _) = review(&mut app, work, None, None); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!(app.page, Page::Diagram); assert_eq!(app.selected, target); assert!( app.project().diagrams[&app.active_diagram.unwrap()] .nodes .values() .any(|n| n.element == target) ); } #[test] fn machine_and_typed_action_transitions_resolve_the_owner_and_reusing_that_context_retains_camera() { for selected_action in [false, true] { let mut p = Project::empty(); let owner = element(&mut p, 1, Kind::Entity, Layer::Oa, "Control room", None); let counterpart = element(&mut p, 10, Kind::System, Layer::Sa, "Control room", None); let machine = element( &mut p, 100, Kind::StateMachine, Layer::Oa, "Pressure modes", Some(owner), ); let region = element(&mut p, 101, Kind::Region, Layer::Oa, "Main", Some(machine)); let state = element(&mut p, 102, Kind::State, Layer::Oa, "Waiting", Some(region)); let work = element(&mut p, 2, Kind::Activity, Layer::Oa, "Assess alarm", None); let action = Id::from_u128(103); p.relations.insert( action, Relation { id: action, alias: "assess_entry".into(), name: "Assess on entry".into(), description: String::new(), body: RelationBody::StateAction { source: state, target: work, phase: StateActionKind::Entry, }, }, ); let mut app = App::with_project(p); app.page = Page::Behavior; app.behavior.active_owner = Some(machine); let selected = if selected_action { action } else { machine }; let (target, expected) = review(&mut app, selected, Some((owner, counterpart)), None); let target_machine = crate::behavior::selected_behavior(&expected, target).unwrap(); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!(app.project(), &expected); assert_eq!(app.page, Page::Behavior); assert_eq!(app.selected, target); assert_eq!(app.behavior.active_owner, Some(target_machine)); assert!( app.project().diagrams.is_empty(), "The state workspace must offer explicit view creation" ); app.behavior.diagram.zoom = 1.17; app.behavior.diagram.pan = iced::Vector::new(-73., 146.); let epoch = app.canvas_epoch; let (same_target, _) = review(&mut app, selected, Some((owner, counterpart)), None); assert_eq!(same_target, target); send(&mut app, Message::Workflow(Event::Apply)); assert_eq!(app.behavior.diagram.zoom, 1.17); assert_eq!(app.behavior.diagram.pan, iced::Vector::new(-73., 146.)); assert_eq!(app.canvas_epoch, epoch); } } #[test] fn rejected_source_writeback_keeps_review_and_navigation_and_invalid_draft_still_blocks_start() { let (mut app, root, work, reused) = fixture(Layer::Oa, Kind::Scenario); let bundle = app.document.bundle().clone(); let mut limits = oa_core::source_project::ProjectLimits::default(); limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::() + 1; app.document = Document::from_bundle(bundle.clone(), limits).unwrap(); let (_, _) = review(&mut app, root, Some((work, reused)), None); let before = app.document.snapshot(); send(&mut app, Message::Workflow(Event::Apply)); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); assert!(app.workflow.is_some()); assert_eq!(app.page, Page::Behavior); assert_eq!(app.selected, root); assert_eq!(app.behavior.active_owner, Some(root)); assert!(!app.document.can_undo()); app.workflow = None; send( &mut app, Message::Field("/alias".into(), "not an alias!".into()), ); let draft = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); send(&mut app, Message::StartTransition); assert!(app.workflow.is_none()); assert_eq!( serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), draft ); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); }