//! Actual arrangement selection and current-record Details admission. use super::*; use crate::behavior::canvas_workspace::tests::Mounted; use crate::behavior::scenario::{CanvasEvent, Context, Shelf}; use crate::behavior::scenario_layout::Representation; use iced::{Point, Size, advanced::widget}; const FIXTURE: &str = include_str!("diagram_usages/tests/saved_scenario.arcadia"); fn id(app: &App, alias: &str) -> Id { app.project() .identities() .into_iter() .find(|(_, name)| *name == alias) .unwrap() .0 } fn setup() -> (App, Id, Id, Id) { let mut app = App::with_project(oa_core::language::parse(FIXTURE).unwrap()); let a = id(&app, "incident_readable"); let b = id(&app, "incident_wide"); let owner = id(&app, "incident_response"); let _ = app.update(Message::Select(owner)); let _ = app.update(Message::Page(Page::Behavior)); choose(&mut app, Representation::Saved { diagram: a }); let _ = app.update(Message::Select(a)); app.window_size = Size::new(1440., 1000.); (app, a, b, owner) } fn send(app: &mut App, event: impl FnOnce(Context) -> CanvasEvent) { let context = app.scenario_context().unwrap(); let _ = app.update(Message::ScenarioCanvas(event(context))); } fn choose(app: &mut App, representation: Representation) { send(app, |context| { CanvasEvent::Representation(context, representation) }); } fn model_state(app: &App) -> (u64, bool, bool, bool) { ( app.document.revision(), app.document.dirty(), app.document.can_undo(), app.document.can_redo(), ) } #[tokio::test] async fn actual_arrangement_picker_follows_selected_saved_record_and_opens_b_properties() { let (mut app, a, b, _) = setup(); let mut mounted = Mounted::new(&mut app).await; let snapshot = app.document.snapshot(); let before = model_state(&app); let captured = app.scenario_context().unwrap(); let picker = mounted.geometry(&app).bounds("scenario-arrangement-picker"); assert!( app.window_size.height - (picker.y + picker.height) > picker.y, "fixture has room for Iced's menu below the actual picker" ); assert_eq!(app.selected, a); mounted.click(&mut app, picker.center()).await; // iced_widget 0.14.2 overlay/menu.rs uses default line height plus // vertical padding for each row. The real workspace orders Automatic, // then the fixture's A/B/C Diagram IDs. Select B from the actual overlay. let row_height = f32::from(iced::advanced::text::LineHeight::default().to_absolute(12.into())) + 10.; let b_row = Point::new( picker.center_x(), picker.y + picker.height + row_height * 2.5, ); assert!(b_row.y < app.window_size.height); let events = mounted.click(&mut app, b_row).await; assert!(events.iter().any(|event| matches!(event, Message::ScenarioCanvas(CanvasEvent::Representation(context, Representation::Saved { diagram })) if *context == captured && *diagram == b)), "must exercise the actual arrangement callback"); assert_eq!(app.selected, b); assert_eq!(app.draft.as_ref().unwrap().original, b); assert_eq!( app.draft.as_ref().unwrap().value["name"], "B - Incident review negative and far" ); assert_eq!(app.active_diagram, Some(b)); assert_eq!( app.scenario_context().unwrap().representation, Representation::Saved { diagram: b } ); assert_ne!(app.scenario_context().unwrap().visit, captured.visit); send(&mut app, |context| { CanvasEvent::Shelf(context, Shelf::Selection) }); mounted.settle(&mut app).await; let geometry = mounted.geometry(&app); let (details, offset) = geometry.scrolls[&widget::Id::new("scenario-details")]; let point = geometry.bounds("scenario-properties").center() - offset; assert!(details.contains(point)); let events = mounted.click(&mut app, point).await; assert!(events.iter().any(|event| matches!(event, Message::ScenarioCanvas(CanvasEvent::Properties(_, selected)) if *selected == b))); mounted.settle(&mut app).await; let geometry = mounted.geometry(&app); let (inspector, offset) = geometry.scrolls[&widget::Id::new("inspector")]; assert!( geometry .text_inputs .iter() .any(|bounds| inspector.contains(bounds.center() - offset)), "current B Properties must expose an actual editable field" ); assert!( !geometry .scrolls .contains_key(&widget::Id::new("scenario-details")) ); assert_eq!( app.inspector_tab, crate::navigation::InspectorTab::Properties ); assert_eq!(app.selected, b); assert_eq!(app.draft.as_ref().unwrap().original, b); assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); assert_eq!(app.project(), &snapshot.decoded.project); assert_eq!(app.document.bundle(), &snapshot.bundle); assert_eq!(model_state(&app), before); } #[test] fn arrangement_change_preserves_independent_semantic_and_foreign_view_selection() { for alias in [ "initial_report", "reporter", "incident_response", "incident_repair", ] { let (mut app, _, b, _) = setup(); let selected = id(&app, alias); let _ = app.update(Message::Select(selected)); let snapshot = app.document.snapshot(); let before = model_state(&app); choose(&mut app, Representation::Saved { diagram: b }); assert_eq!(app.selected, selected, "independent selection {alias}"); assert_eq!(app.draft.as_ref().unwrap().original, selected); assert_eq!(app.active_diagram, Some(b)); assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); assert_eq!(app.document.bundle(), &snapshot.bundle); assert_eq!(model_state(&app), before); if alias == "incident_repair" { assert!(!behavior::scenario::properties_target( app.project(), app.scenario_context().unwrap(), selected )); } } } #[test] fn arrangement_follows_view_record_to_automatic_owner_but_not_owner_back_to_saved() { let (mut app, _, b, owner) = setup(); let snapshot = app.document.snapshot(); let before = model_state(&app); choose(&mut app, Representation::Derived { owner }); assert_eq!(app.selected, owner); assert_eq!(app.draft.as_ref().unwrap().original, owner); assert_eq!(app.active_diagram, None); assert!(behavior::scenario::properties_target( app.project(), app.scenario_context().unwrap(), owner )); choose(&mut app, Representation::Saved { diagram: b }); assert_eq!( app.selected, owner, "Scenario owner is now independent semantic selection" ); assert_eq!(app.draft.as_ref().unwrap().original, owner); assert_eq!(app.active_diagram, Some(b)); assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); assert_eq!(app.document.bundle(), &snapshot.bundle); assert_eq!(model_state(&app), before); } #[test] fn arrangement_applies_only_the_old_view_draft_then_follows_b_with_one_undo() { let (mut app, a, b, _) = setup(); let snapshot = app.document.snapshot(); let mut expected = app.project().clone(); expected.diagrams.get_mut(&a).unwrap().name = "Reviewed A".into(); let revision = app.document.revision(); let _ = app.update(Message::Field("/name".into(), "Reviewed A".into())); choose(&mut app, Representation::Saved { diagram: b }); assert_eq!(app.project(), &expected); assert_eq!(app.selected, b); assert_eq!(app.draft.as_ref().unwrap().original, b); assert!(!app.draft_dirty()); assert_eq!(app.document.revision(), revision + 1); let accepted = app.document.snapshot(); let _ = app.update(Message::Undo); assert_eq!(app.project(), &snapshot.decoded.project); assert_eq!(app.document.bundle(), &snapshot.bundle); assert!(!app.document.can_undo() && app.document.can_redo()); let _ = app.update(Message::Redo); assert_eq!(app.project(), &expected); assert_eq!(app.document.bundle(), &accepted.bundle); assert!(app.document.can_undo() && !app.document.can_redo()); } #[test] fn same_arrangement_uuid_edit_follows_the_reidentified_record_and_round_trips_source() { let (mut app, a, _, _) = setup(); let snapshot = app.document.snapshot(); let replacement = Id::parse_str("70200000-0000-4000-8000-000000000099").unwrap(); let mut expected = app.project().clone(); let mut diagram = expected.diagrams.remove(&a).unwrap(); diagram.id = replacement; expected.diagrams.insert(replacement, diagram); let _ = app.update(Message::Field("/id".into(), replacement.to_string())); choose(&mut app, Representation::Saved { diagram: a }); assert_eq!(app.project(), &expected); assert_eq!(app.selected, replacement); assert_eq!(app.draft.as_ref().unwrap().original, replacement); assert_eq!(app.active_diagram, Some(replacement)); assert_eq!( app.scenario_context().unwrap().representation, Representation::Saved { diagram: replacement } ); let accepted = app.document.snapshot(); let _ = app.update(Message::Undo); assert_eq!(app.project(), &snapshot.decoded.project); assert_eq!(app.document.bundle(), &snapshot.bundle); assert!(!app.document.can_undo() && app.document.can_redo()); let _ = app.update(Message::Redo); assert_eq!(app.project(), &expected); assert_eq!(app.document.bundle(), &accepted.bundle); assert!(!app.document.can_redo()); } #[test] fn invalid_draft_and_stale_arrangement_preserve_selection_inputs_and_accepted_state() { let (mut app, a, b, _) = setup(); let old = app.scenario_context().unwrap(); let snapshot = app.document.snapshot(); let before = model_state(&app); let _ = app.update(Message::Field("/id".into(), "not-a-uuid".into())); let draft_value = app.draft.as_ref().unwrap().value.clone(); choose(&mut app, Representation::Saved { diagram: b }); assert_eq!(app.selected, a); assert_eq!(app.active_diagram, Some(a)); assert_eq!( app.scenario_context().unwrap().representation, old.representation ); assert_eq!(app.draft.as_ref().unwrap().value, draft_value); assert_eq!(app.draft.as_ref().unwrap().inputs["/id"].raw, "not-a-uuid"); assert!(app.draft_dirty()); assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); assert_eq!(model_state(&app), before); let _ = app.update(Message::RevertProperties); choose(&mut app, Representation::Saved { diagram: b }); let current = app.scenario_context().unwrap(); let _ = app.update(Message::Field("/name".into(), "Retained B input".into())); let selected = app.selected; let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Representation( old, Representation::Saved { diagram: a }, ))); assert_eq!(app.selected, selected); assert_eq!(app.active_diagram, Some(b)); assert_eq!(app.scenario_context().unwrap().visit, current.visit); assert_eq!( app.draft.as_ref().unwrap().inputs["/name"].raw, "Retained B input" ); assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); assert_eq!(app.document.bundle(), &snapshot.bundle); assert_eq!(model_state(&app), before); }