use super::*; use crate::{context_menu::Entry, model_menu, views::traceability::Filter}; fn send(app: &mut App, message: Message) { let _ = app.update(message); } fn fixture() -> (App, Id, Id, Id, Id) { let mut p = Project::empty(); let oa = p .add_element(Kind::Activity, Layer::Oa, "Observe", None) .unwrap(); let sa = p .add_element(Kind::Function, Layer::Sa, "Detect", None) .unwrap(); let mixed = p.ensure_diagram(Layer::Oa); p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability; p.place(mixed, oa, Position { x: 300., y: 200. }); let system = p.ensure_diagram(Layer::Sa); p.place(system, sa, Position { x: 700., y: 200. }); oa_core::validate_candidate(&p).unwrap(); let mut app = App::with_project(p); app.page = Page::Diagram; app.layer = Layer::Oa; app.active_diagram = Some(mixed); app.selected = oa; app.refresh_draft(); app.traceability.filter = Filter::Artifacts; app.traceability.query = "stale artifact query".into(); (app, mixed, system, oa, sa) } fn show_action(app: &mut App, element: Id) -> Message { send(app, model_menu::open(element, iced::Point::ORIGIN)); model_menu::popup(app) .unwrap() .entries .into_iter() .find_map(|entry| match entry { Entry::Action { label, message, .. } if label == "Show in this diagram" => { Some(message) } _ => None, }) .expect("eligible real menu action") .expect("eligible menu action must be enabled") } #[test] fn review_layer_traces_resets_inventory_without_applying_property_or_source_drafts() { for source_draft in [false, true] { let (mut app, mixed, _, oa, _) = fixture(); if source_draft { send(&mut app, Message::Page(Page::Text)); send( &mut app, Message::SourceEdit(iced::widget::text_editor::Action::Edit( iced::widget::text_editor::Edit::Insert('x'), )), ); assert!(app.source_dirty()); } else { send( &mut app, Message::Field("/alias".into(), "invalid alias".into()), ); assert!(app.draft_dirty()); } let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); let source = app.source_editor.text(); let before = app.document.snapshot(); let camera = (app.zoom, app.pan); send(&mut app, Message::ReviewLayerTraces); assert_eq!(app.page, Page::Traces); assert_eq!(app.traceability.filter, Filter::Traces); assert!(app.traceability.query.is_empty()); assert_eq!( (app.layer, app.active_diagram, app.selected), (Layer::Oa, Some(mixed), oa) ); assert_eq!((app.zoom, app.pan), camera); assert_eq!( serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), raw ); assert_eq!(app.source_editor.text(), source); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); assert!(!app.document.can_undo()); } } #[test] fn review_layer_traces_respects_busy_modal_and_active_placement_guards() { for already_traces in [false, true] { for guard in 0..5 { let (mut app, mixed, _, oa, _) = fixture(); match guard { 0 => app.busy = true, 1 => app.delete_pending = true, 2 => app.hosted.visible = true, 3 => send( &mut app, Message::Architecture(architecture_authoring::Event::Existing(None)), ), 4 => { app.page = Page::Behavior; let p = app.project().clone(); app.behavior .creation .update(&p, behavior::creation::Event::Open(Layer::Oa)) .unwrap(); assert!(app.behavior.has_pending()); } _ => unreachable!(), } if already_traces { app.page = Page::Traces; } let original_page = app.page; let before = app.document.snapshot(); let behavior_before = app.behavior.recovery_snapshot().unwrap(); send(&mut app, Message::ReviewLayerTraces); assert_eq!( app.page, original_page, "guard {guard}, already traces {already_traces}" ); assert_eq!(app.traceability.filter, Filter::Artifacts); assert_eq!(app.traceability.query, "stale artifact query"); assert_eq!( (app.layer, app.active_diagram, app.selected), (Layer::Oa, Some(mixed), oa) ); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); assert_eq!(app.behavior.recovery_snapshot().unwrap(), behavior_before); if guard == 3 { assert!(app.architecture.is_some()); } } } } #[test] fn mixed_view_show_keeps_its_context_for_every_diagram_app_and_target_layer() { for diagram_layer in [Layer::Oa, Layer::Sa] { for app_layer in [Layer::Oa, Layer::Sa] { for target_layer in [Layer::Oa, Layer::Sa] { let mut p = Project::empty(); let oa = p .add_element(Kind::Activity, Layer::Oa, "Observe", None) .unwrap(); let sa = p .add_element(Kind::Function, Layer::Sa, "Detect", None) .unwrap(); let target = if target_layer == Layer::Oa { oa } else { sa }; let other = if target_layer == Layer::Oa { sa } else { oa }; let mixed = p.ensure_diagram(diagram_layer); p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability; // An attractive same-layer alternative makes accidental // semantic-selection navigation observable in both directions. let mut alternative = p.diagrams[&mixed].clone(); alternative.id = Id::new_v4(); alternative.alias = "same_layer_alternative".into(); alternative.layer = target_layer; alternative.projection = ProjectionKind::Overview; let alternative_id = alternative.id; p.diagrams.insert(alternative_id, alternative); p.place(alternative_id, target, Position { x: 700., y: 200. }); oa_core::validate_candidate(&p).unwrap(); let mut app = App::with_project(p); app.page = Page::Diagram; app.layer = app_layer; app.active_diagram = Some(mixed); app.selected = other; app.refresh_draft(); let before = app.project().clone(); let action = show_action(&mut app, target); send(&mut app, action); // The existing RevealInDiagram action synchronizes the layer // to the captured view, even if the prior perspective differed. assert_eq!( (app.active_diagram, app.layer, app.selected), (Some(mixed), diagram_layer, target) ); let added = app.project().diagrams[&mixed] .nodes .values() .find(|n| n.element == target) .unwrap() .clone(); assert_eq!(app.selected_node, Some(added.id)); let mut expected = before; expected .diagrams .get_mut(&mixed) .unwrap() .nodes .insert(added.id, added); assert_eq!(app.project(), &expected); } } } } #[test] fn mixed_view_show_menu_places_in_captured_view_with_exact_undo_redo() { let (mut app, mixed, system, _, sa) = fixture(); let before = app.document.snapshot(); let action = show_action(&mut app, sa); send(&mut app, action); assert_eq!( (app.active_diagram, app.layer, app.selected), (Some(mixed), Layer::Oa, sa) ); let added = app.project().diagrams[&mixed] .nodes .values() .find(|n| n.element == sa) .unwrap() .clone(); assert_eq!(added.position, Position { x: 60., y: 60. }); let mut expected = before.decoded.project.clone(); expected .diagrams .get_mut(&mixed) .unwrap() .nodes .insert(added.id, added); assert_eq!(app.project(), &expected); assert_eq!( app.project().diagrams[&system], before.decoded.project.diagrams[&system] ); let after = app.document.snapshot(); send(&mut app, Message::Undo); assert_eq!(app.project(), &before.decoded.project); assert_eq!(app.document.bundle(), &before.bundle); assert!(!app.document.can_undo()); send(&mut app, Message::Redo); assert_eq!(app.project(), &after.decoded.project); assert_eq!(app.document.bundle(), &after.bundle); } #[test] fn mixed_view_show_menu_rejects_stale_context_and_pending_property_changes() { for refusal in 0..3 { let (mut app, mixed, system, oa, sa) = fixture(); let action = show_action(&mut app, sa); match refusal { 0 => app.active_diagram = Some(system), 1 => app.canvas_epoch = app.canvas_epoch.wrapping_add(1), 2 => { // A queued action must not apply a draft that became dirty // after the menu was rendered, even when its context is valid. app.draft .as_mut() .unwrap() .input("/name", "Unapplied name".into()); assert!(app.draft_dirty()); } _ => unreachable!(), } let before = app.document.snapshot(); let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); send(&mut app, action); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); assert_eq!(app.selected, oa); assert_eq!( app.active_diagram, Some(if refusal == 0 { system } else { mixed }) ); assert_eq!( serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), raw ); assert!(!app.document.can_undo()); } }