use super::*; use crate::app::architecture_authoring::{Event, kinds}; fn setup(layer: Layer) -> App { let mut app = App::with_project(Project::empty()); app.layer = layer; app.active_diagram = None; let _ = app.update(Message::AddDiagram); app.zoom = 0.75; app.pan = iced::Vector::new(37., -19.); app } fn send(app: &mut App, event: Event) { let _ = app.update(Message::Architecture(event)); } #[test] fn architecture_creation_names_before_commit_preserves_camera_and_undoes_once() { let mut app = setup(Layer::Oa); let before = app.document.bundle().clone(); let model = app.project().clone(); let camera = (app.zoom, app.pan); send(&mut app, Event::Start(Kind::Entity, None)); assert_eq!(app.project(), &model); let context = app.route_context().unwrap(); send( &mut app, Event::Click(context, Position { x: 219., y: 117. }), ); assert_eq!(app.project(), &model); assert!(app.architecture.as_ref().unwrap().inline()); send(&mut app, Event::Name(" Response team 雨 ".into())); send(&mut app, Event::Submit); assert!(app.architecture.is_none()); assert_eq!((app.zoom, app.pan), camera); let element = &app.project().elements[&app.selected]; assert_eq!(element.name, " Response team 雨 "); assert_eq!(element.parent, None); let node = &app.project().diagrams[&app.active_diagram.unwrap()].nodes[&app.selected_node.unwrap()]; assert_eq!(node.position, Position { x: 219., y: 117. }); let accepted = app.document.bundle().clone(); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before); let _ = app.update(Message::Redo); assert_eq!(app.document.bundle(), &accepted); } #[test] fn architecture_context_choices_are_small_and_system_is_singleton() { let mut app = setup(Layer::Oa); assert_eq!(kinds(&app), vec![Kind::Entity, Kind::Actor, Kind::Activity]); app = setup(Layer::Sa); assert_eq!( kinds(&app), vec![Kind::System, Kind::SystemActor, Kind::Function] ); send( &mut app, Event::Start(Kind::System, Some(Position { x: 60., y: 60. })), ); send(&mut app, Event::Name("Pump controller".into())); send(&mut app, Event::Submit); assert_eq!(kinds(&app), vec![Kind::SystemActor, Kind::Function]); } #[test] fn architecture_name_survives_rejected_click_resize_navigation_and_escape_is_explicit() { let mut app = setup(Layer::Oa); let before = app.document.bundle().clone(); send(&mut app, Event::Start(Kind::Activity, None)); send(&mut app, Event::Name("Diagnose pressure".into())); send(&mut app, Event::Start(Kind::Entity, None)); assert!( matches!(&app.architecture.as_ref().unwrap().saved.intent, architecture_authoring::Intent::Create { name, kind: Kind::Activity, .. } if name == "Diagnose pressure") ); send(&mut app, Event::Submit); let mut stale = app.route_context().unwrap(); stale.revision += 1; send(&mut app, Event::Click(stale, Position { x: 1., y: 2. })); let _ = app.update(Message::Page(Page::Home)); let _ = app.update(Message::WindowResized(iced::Size::new(1100., 700.))); assert_eq!(app.page, Page::Diagram); assert!(app.architecture.is_some()); assert_eq!(app.document.bundle(), &before); let _ = app.update(Message::Escape); assert!(app.architecture.is_none()); assert_eq!(app.document.bundle(), &before); } #[test] fn architecture_existing_inventory_is_contextual_and_placement_never_implies_allocation() { let mut app = setup(Layer::Oa); let entity = app .project() .make_element(Kind::Entity, Layer::Oa, "Response team", None) .unwrap(); let entity_id = entity.id; let work = app .project() .make_element(Kind::Activity, Layer::Oa, "Report status", None) .unwrap(); let work_id = work.id; let data = app .project() .make_element(Kind::DataType, Layer::Oa, "Pressure", None) .unwrap(); let data_id = data.id; assert!(app.execute(Command::Batch(vec![ Command::CreateElement(entity), Command::CreateElement(work), Command::CreateElement(data) ]))); send(&mut app, Event::Existing(Some(Position { x: 80., y: 80. }))); let ids: Vec<_> = architecture_authoring::candidates(&app) .iter() .map(|row| row.0) .collect(); assert!(ids.contains(&entity_id) && ids.contains(&work_id)); assert!(!ids.contains(&data_id)); send(&mut app, Event::Pick(entity_id)); let semantic = app.project().elements.clone(); let before = app.document.bundle().clone(); let camera = (app.zoom, app.pan); send( &mut app, Event::Existing(Some(Position { x: 90., y: 100. })), ); assert!( !architecture_authoring::candidates(&app) .iter() .any(|row| row.0 == entity_id) ); send(&mut app, Event::ShowPlaced(true)); assert!( architecture_authoring::candidates(&app) .iter() .any(|row| row.0 == entity_id && row.2) ); send(&mut app, Event::Pick(work_id)); assert_eq!(app.project().elements, semantic); assert!(app.project().relations.is_empty()); assert_eq!((app.zoom, app.pan), camera); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before); } #[test] fn architecture_checkpoint_retains_exact_name_point_and_fresh_binding_without_commands() { for (name, placing) in [(" Team 雨 ", true), (" ", false)] { let mut app = setup(Layer::Oa); send(&mut app, Event::Start(Kind::Entity, None)); send(&mut app, Event::Name(name.into())); if placing { send(&mut app, Event::Submit); } app.architecture.as_mut().unwrap().saved.point = Some(Position { x: 127.5, y: 215. }); app.architecture.as_mut().unwrap().saved.placing = placing; let snapshot = crate::recovery::Snapshot::capture( app.canvas_document, &app.document, &app.source_workspace, app.draft.as_ref(), &app.behavior, None, app.selected, ) .unwrap() .with_architecture(app.architecture.as_ref()); let bytes = serde_json::to_vec(&snapshot).unwrap(); let decoded: crate::recovery::Snapshot = serde_json::from_slice(&bytes).unwrap(); let accepted = app.document.bundle().clone(); let mut restored = setup(Layer::Sa); let _ = restored.update(Message::CheckpointLoaded(Ok(Arc::new(decoded)))); assert_eq!(restored.document.bundle(), &accepted); let draft = &restored.architecture.as_ref().unwrap().saved; assert!( matches!(&draft.intent, architecture_authoring::Intent::Create { name: actual, .. } if actual == name) ); assert_eq!(draft.placing, placing); assert_eq!(draft.point, Some(Position { x: 127.5, y: 215. })); if placing { let context = restored.route_context().unwrap(); send( &mut restored, Event::Click(context, Position { x: 20., y: 30. }), ); assert!(restored.architecture.as_ref().unwrap().inline()); send(&mut restored, Event::Submit); assert!(restored.architecture.is_none()); assert_eq!(restored.project().elements[&restored.selected].name, name); } else { send(&mut restored, Event::Submit); assert!(restored.architecture.is_some()); assert_eq!(restored.document.bundle(), &accepted); } let mut value: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); value["architecture"]["base"] = json!("stale"); assert!( serde_json::from_value::(value) .unwrap() .restore() .is_err() ); } } #[test] fn architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan() { use iced::widget::canvas::Program; let mut app = setup(Layer::Oa); send(&mut app, Event::Start(Kind::Actor, None)); send(&mut app, Event::Name("Control centre".into())); send(&mut app, Event::Submit); let bounds = iced::Rectangle { x: 260., y: 180., width: 600., height: 380., }; let local = iced::Point::new(190., 100.); let expected = Position { x: (local.x - app.pan.x) / app.zoom, y: (local.y - app.pan.y) / app.zoom, }; let canvas = crate::diagram::DiagramCanvas { project: app.project(), view_index: None, diagram: app.active_diagram.unwrap(), selected: app.selected, navigation_focus: None, zoom: app.zoom, pan: app.pan, context: app.route_context().unwrap(), selected_bend: None, editable: true, placing: true, connection: None, }; let mut state = crate::diagram::State::default(); let action = canvas .update( &mut state, &iced::widget::canvas::Event::Mouse(iced::mouse::Event::ButtonPressed( iced::mouse::Button::Left, )), bounds, iced::mouse::Cursor::Available(iced::Point::new( bounds.x + local.x, bounds.y + local.y, )), ) .unwrap(); let message = action .into_inner() .0 .expect("named placement emits a command"); let _ = app.update(message); assert_eq!( app.architecture.as_ref().unwrap().saved.point, Some(expected) ); send(&mut app, Event::Submit); let node = &app.project().diagrams[&app.active_diagram.unwrap()].nodes[&app.selected_node.unwrap()]; assert_eq!(node.position, expected); } #[test] fn architecture_cancelled_gesture_cannot_commit_a_later_named_draft() { let mut app = setup(Layer::Oa); send(&mut app, Event::Start(Kind::Entity, None)); send(&mut app, Event::Name("First".into())); send(&mut app, Event::Submit); let stale = app.route_context().unwrap(); send(&mut app, Event::Cancel); send(&mut app, Event::Start(Kind::Actor, None)); send(&mut app, Event::Name("Second".into())); send(&mut app, Event::Submit); let before = app.document.bundle().clone(); send(&mut app, Event::Click(stale, Position { x: 1., y: 1. })); assert_eq!(app.document.bundle(), &before); assert!(app.architecture.is_some()); } #[test] fn architecture_f2_rename_preserves_identity_placement_and_atomic_history() { use iced::{keyboard, mouse, widget::canvas::Program}; let mut app = setup(Layer::Oa); send( &mut app, Event::Start(Kind::Entity, Some(Position { x: 200., y: 120. })), ); send(&mut app, Event::Name("Response team".into())); send(&mut app, Event::Submit); let before = app.document.bundle().clone(); let element = app.selected; let node = app.selected_node; let diagrams = app.project().diagrams.clone(); let camera = (app.zoom, app.pan); let canvas = crate::diagram::DiagramCanvas { project: app.project(), view_index: None, diagram: app.active_diagram.unwrap(), selected: element, navigation_focus: None, zoom: app.zoom, pan: app.pan, context: app.route_context().unwrap(), selected_bend: None, editable: true, placing: false, connection: None, }; let bounds = iced::Rectangle::with_size(iced::Size::new(800., 600.)); let cursor = mouse::Cursor::Available(iced::Point::new(500., 400.)); let mut state = crate::diagram::State::default(); let key = iced::widget::canvas::Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::F2), modified_key: keyboard::Key::Named(keyboard::key::Named::F2), physical_key: keyboard::key::Physical::Code(keyboard::key::Code::F2), location: keyboard::Location::Standard, modifiers: keyboard::Modifiers::empty(), text: None, repeat: false, }); assert!(canvas.update(&mut state, &key, bounds, cursor).is_none()); let _ = canvas.update( &mut state, &iced::widget::canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), bounds, cursor, ); let message = canvas .update(&mut state, &key, bounds, cursor) .expect("focused F2 opens the selected semantic name") .into_inner() .0 .unwrap(); let _ = app.update(message); assert!(app.architecture.as_ref().unwrap().inline()); send(&mut app, Event::Name(" Emergency team 雨 ".into())); assert_eq!(app.document.bundle(), &before); send(&mut app, Event::Submit); assert!(app.architecture.is_none()); assert_eq!((app.selected, app.selected_node), (element, node)); assert_eq!( app.project().elements[&element].name, " Emergency team 雨 " ); assert_eq!(app.project().diagrams, diagrams); assert_eq!((app.zoom, app.pan), camera); let accepted = app.document.bundle().clone(); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before); let _ = app.update(Message::Redo); assert_eq!(app.document.bundle(), &accepted); let context = app.route_context().unwrap(); send( &mut app, Event::Rename(context, element, Position { x: 200., y: 120. }), ); send(&mut app, Event::Name("Cancelled name".into())); let _ = app.update(Message::Escape); assert!(app.architecture.is_none()); assert_eq!(app.document.bundle(), &accepted); } #[test] fn architecture_contextual_allocation_creates_work_relation_and_node_in_one_undo() { for (layer, performer_kind, work_kind) in [ (Layer::Oa, Kind::Entity, Kind::Activity), (Layer::Sa, Kind::System, Kind::Function), ] { let mut app = setup(layer); send( &mut app, Event::Start(performer_kind, Some(Position { x: 20., y: 20. })), ); send(&mut app, Event::Name("Control team".into())); send(&mut app, Event::Submit); let performer = app.selected; let before = app.document.bundle().clone(); send( &mut app, Event::Allocate(work_kind, performer, Some(Position { x: 55., y: 90. })), ); send(&mut app, Event::Name("Assess pressure".into())); assert_eq!(app.document.bundle(), &before); send(&mut app, Event::Submit); assert!(app.architecture.is_none(), "{}", app.status); let work = app.selected; assert_eq!(app.project().elements[&work].parent, None); assert!( app.project() .relations .values() .any(|r| r.kind() == RelationKind::Allocation && r.source() == performer && r.target() == work) ); let node = &app.project().diagrams[&app.active_diagram.unwrap()].nodes [&app.selected_node.unwrap()]; assert_eq!(node.position, Position { x: 55., y: 90. }); let accepted = app.document.bundle().clone(); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before); let _ = app.update(Message::Redo); assert_eq!(app.document.bundle(), &accepted); } } #[test] fn architecture_rejected_submission_keeps_inline_name_visible_and_invalid_click_is_ignored() { let mut app = setup(Layer::Oa); send(&mut app, Event::Start(Kind::Entity, None)); let context = app.route_context().unwrap(); send( &mut app, Event::Click( context, Position { x: f32::NAN, y: 30., }, ), ); assert!(app.architecture.as_ref().unwrap().unnamed_tool()); send(&mut app, Event::Click(context, Position { x: 40., y: 30. })); send(&mut app, Event::Name("Retained team".into())); // A replaced document must not accept a queued draft submission. app.canvas_document = Id::new_v4(); let before = app.document.bundle().clone(); send(&mut app, Event::Submit); let draft = app.architecture.as_ref().unwrap(); assert!(draft.inline()); assert!(draft.error.is_some()); assert!( matches!(&draft.saved.intent, architecture_authoring::Intent::Create { name, .. } if name == "Retained team") ); assert_eq!(app.document.bundle(), &before); let _ = app.update(Message::Escape); assert!(app.architecture.is_none()); assert_eq!(app.document.bundle(), &before); } #[test] fn basic_transition_explicit_overview_focus_wins_over_an_already_populated_architecture() { use crate::workflow::{Event as W, Workflow}; let mut p = Project::empty(); let work = p .add_element(Kind::Activity, Layer::Oa, "Report status", None) .unwrap(); let architecture = p.ensure_diagram(Layer::Sa); p.diagrams.get_mut(&architecture).unwrap().projection = ProjectionKind::Allocation; let mut first = Workflow::transition(&p, work).unwrap(); let Workflow::Transition(t) = &first else { unreachable!() }; let target = t .plan .changes .iter() .find(|c| c.source == work) .unwrap() .target; let mut p = first.update(&p, W::Apply).unwrap().unwrap(); let mut overview = p.diagrams[&architecture].clone(); overview.id = Id::new_v4(); overview.alias = "chosen_overview".into(); overview.name = "Chosen system overview".into(); overview.projection = ProjectionKind::Overview; overview.nodes.clear(); let chosen = overview.id; p.diagrams.insert(chosen, overview); let placement = p.place(chosen, target, Position { x: 823., y: 417. }); let mut app = App::with_project(p); app.selected = work; app.active_diagram = Some(architecture); app.refresh_draft(); let _ = app.update(Message::StartTransition); let _ = app.update(Message::Workflow(W::Destination(Choice { id: chosen, label: "Chosen system overview".into(), }))); let before = app.project().diagrams.clone(); let _ = app.update(Message::Workflow(W::Apply)); assert!(app.workflow.is_none(), "{}", app.status); assert_eq!(app.active_diagram, Some(chosen)); assert_eq!(app.selected, target); assert_eq!(app.selected_node, Some(placement)); assert_eq!(app.project().diagrams, before); }