use super::*; 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)); } fn create(app: &mut App, kind: Kind, name: &str) -> Id { send(app, Event::Start(kind, Some(Position { x: 120., y: 160. }))); send(app, Event::Name(name.into())); send(app, Event::Submit); assert!(app.architecture.is_none(), "{}", app.status); app.selected } #[test] fn architecture_allocated_work_is_root_owned_and_one_exact_history_change() { for (layer, participant, work) in [ (Layer::Oa, Kind::Entity, Kind::Activity), (Layer::Sa, Kind::System, Kind::Function), ] { let mut app = setup(layer); let performer = create(&mut app, participant, "Control team"); let before = app.document.bundle().clone(); let camera = (app.zoom, app.pan); let point = Position { x: 151., y: 221. }; send(&mut app, Event::Allocate(work, performer, Some(point))); send(&mut app, Event::Name(" Report 雨 ".into())); assert_eq!(app.document.bundle(), &before); send(&mut app, Event::Submit); assert!(app.architecture.is_none(), "{}", app.status); let created = app.selected; let element = &app.project().elements[&created]; assert_eq!(element.kind(), work); assert_eq!(element.name, " Report 雨 "); assert_eq!(element.parent, None); assert!(app.project().relations.values().any(|r| { r.kind() == RelationKind::Allocation && r.source() == performer && r.target() == created })); let d = &app.project().diagrams[&app.active_diagram.unwrap()]; assert_eq!(d.nodes[&app.selected_node.unwrap()].position, point); 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); } } #[test] fn architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo() { use iced::widget::canvas::{Event as CanvasEvent, Program}; use iced::{keyboard, mouse}; let mut app = setup(Layer::Oa); let element = create(&mut app, Kind::Entity, "Original team"); let before = app.document.bundle().clone(); let diagrams = app.project().diagrams.clone(); let camera = (app.zoom, app.pan); let bounds = iced::Rectangle::new(iced::Point::ORIGIN, iced::Size::new(600., 380.)); 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 mut state = crate::diagram::State::default(); let key = CanvasEvent::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, mouse::Cursor::Unavailable) .is_none() ); let _ = canvas.update( &mut state, &CanvasEvent::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), bounds, mouse::Cursor::Available(iced::Point::new(150., 120.)), ); let action = canvas .update(&mut state, &key, bounds, mouse::Cursor::Unavailable) .unwrap(); let message = action.into_inner().0.unwrap(); let _ = app.update(message); assert!(app.architecture.as_ref().unwrap().inline()); assert!(matches!(&app.architecture.as_ref().unwrap().saved.intent, Intent::Rename { element: id, name } if *id == element && name == "Original team")); send(&mut app, Event::Name(" Renamed team 雨 ".into())); assert_eq!(app.document.bundle(), &before); send(&mut app, Event::Submit); assert!(app.architecture.is_none(), "{}", app.status); assert_eq!(app.project().elements[&element].name, " Renamed 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); } #[test] fn architecture_blank_names_and_escape_never_accept_placeholder_or_rename() { let mut app = setup(Layer::Oa); let element = create(&mut app, Kind::Actor, "Dispatcher"); let before = app.document.bundle().clone(); let context = app.route_context().unwrap(); for event in [ Event::Start(Kind::Activity, Some(Position { x: 220., y: 330. })), Event::Rename(context, element, Position { x: 150., y: 200. }), ] { // Every cancel changes the gesture context; use the current context for F2. let event = match event { Event::Rename(_, element, point) => { Event::Rename(app.route_context().unwrap(), element, point) } event => event, }; send(&mut app, event); send(&mut app, Event::Name(" \t ".into())); send(&mut app, Event::Submit); assert!(app.architecture.as_ref().unwrap().inline()); assert!(app.architecture.as_ref().unwrap().error.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_invalid_click_cannot_poison_inline_or_checkpoint_geometry() { let mut app = setup(Layer::Oa); send(&mut app, Event::Start(Kind::Entity, None)); let before = app.document.bundle().clone(); let context = app.route_context().unwrap(); for point in [ Position { x: f32::NAN, y: 0. }, Position { x: 0., y: f32::INFINITY, }, ] { send(&mut app, Event::Click(context, point)); let draft = app.architecture.as_ref().unwrap(); assert!(draft.unnamed_tool()); assert!(validate_saved(app.project(), &draft.saved).is_ok()); assert_eq!(app.document.bundle(), &before); } send(&mut app, Event::Click(context, Position { x: 25., y: 30. })); assert!(app.architecture.as_ref().unwrap().inline()); } #[test] fn architecture_stale_submit_keeps_create_and_rename_visible_and_exact() { for rename in [false, true] { for stale_document in [false, true] { let mut app = setup(Layer::Oa); let element = create(&mut app, Kind::Entity, "Accepted team"); let point = Position { x: 151.5, y: 227.25, }; let event = if rename { Event::Rename(app.route_context().unwrap(), element, point) } else { Event::Start(Kind::Activity, Some(point)) }; send(&mut app, event); send( &mut app, Event::Name(" Retained engineering name 雨 ".into()), ); // Change only the binding. The accepted model/source must not be // rewritten or a rejected draft silently rebound to the new context. if stale_document { app.architecture.as_mut().unwrap().document = Id::new_v4(); } else { app.architecture.as_mut().unwrap().revision = app.document.revision() + 1; } let draft = app.architecture.as_ref().unwrap(); let saved = serde_json::to_value(&draft.saved).unwrap(); let binding = (draft.document, draft.revision); let model = app.project().clone(); let source = app.document.bundle().clone(); let revision = app.document.revision(); let camera = (app.zoom, app.pan); for _ in 0..2 { send(&mut app, Event::Submit); let draft = app.architecture.as_ref().unwrap(); assert!( draft.inline(), "rename={rename}, stale_document={stale_document}" ); assert!(inline_editor(&app).is_some()); assert_eq!(draft.error.as_deref(), Some(app.status.as_str())); assert!(app.status.contains("cancel and restart")); assert_eq!(serde_json::to_value(&draft.saved).unwrap(), saved); assert_eq!((draft.document, draft.revision), binding); assert_eq!(app.project(), &model); assert_eq!(app.document.bundle(), &source); assert_eq!(app.document.revision(), revision); assert_eq!((app.zoom, app.pan), camera); } let _ = app.update(Message::Escape); assert!(app.architecture.is_none()); assert_eq!(app.project(), &model); assert_eq!(app.document.bundle(), &source); let fresh = create(&mut app, Kind::Actor, "Fresh context"); assert_eq!(app.project().elements[&fresh].name, "Fresh context"); let _ = app.update(Message::Undo); assert_eq!(app.project(), &model); assert_eq!(app.document.bundle(), &source); } } } fn key_event(named: iced::keyboard::key::Named) -> iced::Event { use iced::keyboard; iced::Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(named), modified_key: keyboard::Key::Named(named), physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Escape), location: keyboard::Location::Standard, modifiers: keyboard::Modifiers::empty(), text: None, repeat: false, }) } fn focused_text_input_escape(name: &str) -> Message { use iced::advanced::{Layout, Shell, layout, widget}; let mut field: iced::Element<'_, (), iced::Theme, ()> = text_input("Engineering name", name) .id("architecture-name") .on_input(|_| ()) .into(); let mut tree = widget::Tree::new(field.as_widget()); let size = iced::Size::new(300., 40.); let node = field .as_widget_mut() .layout(&mut tree, &(), &layout::Limits::new(iced::Size::ZERO, size)); tree.state .downcast_mut::>() .focus(); let escape = key_event(iced::keyboard::key::Named::Escape); let mut messages = Vec::new(); let mut shell = Shell::new(&mut messages); field.as_widget_mut().update( &mut tree, &escape, Layout::new(&node), iced::mouse::Cursor::Unavailable, &(), &mut iced::advanced::clipboard::Null, &mut shell, &iced::Rectangle::new(iced::Point::ORIGIN, size), ); assert!( shell.is_event_captured(), "Iced consumes focused-input Escape" ); assert!( !tree .state .downcast_ref::>() .is_focused() ); captured_escape( escape, iced::event::Status::Captured, iced::window::Id::unique(), ) .unwrap() } #[test] fn architecture_first_focused_input_escape_cancels_create_and_rename() { for rename in [false, true] { let mut app = setup(Layer::Oa); let element = create(&mut app, Kind::Actor, "Accepted dispatcher"); let model = app.project().clone(); let source = app.document.bundle().clone(); let revision = app.document.revision(); let camera = (app.zoom, app.pan); let point = Position { x: 439., y: 328.2 }; let event = if rename { Event::Rename(app.route_context().unwrap(), element, point) } else { Event::Start(Kind::Actor, Some(point)) }; send(&mut app, event); let name = " Unaccepted long engineering name 雨 "; send(&mut app, Event::Name(name.into())); assert!(app.architecture.as_ref().unwrap().inline()); let message = focused_text_input_escape(name); let _ = app.update(message); assert!(app.architecture.is_none()); assert!(inline_editor(&app).is_none()); assert_eq!(app.status, "Cancelled · Select / move"); assert_eq!(app.project(), &model); assert_eq!(app.document.bundle(), &source); assert_eq!(app.document.revision(), revision); assert_eq!((app.zoom, app.pan), camera); } } #[test] fn architecture_captured_escape_does_not_cancel_other_controls_or_placement() { let mut app = setup(Layer::Oa); for event in [ None, Some(Event::Start(Kind::Entity, None)), Some(Event::Existing(None)), ] { send(&mut app, Event::Cancel); if let Some(event) = event { send(&mut app, event); } let saved = app .architecture .as_ref() .map(|s| serde_json::to_value(&s.saved).unwrap()); let before = app.document.bundle().clone(); let status = app.status.clone(); let epoch = app.canvas_epoch; let message = focused_text_input_escape("Other field or existing-element search"); let _ = app.update(message); assert_eq!( app.architecture .as_ref() .map(|s| serde_json::to_value(&s.saved).unwrap()), saved ); assert_eq!(app.document.bundle(), &before); assert_eq!(app.status, status); assert_eq!(app.canvas_epoch, epoch); } assert!( captured_escape( key_event(iced::keyboard::key::Named::Escape), iced::event::Status::Ignored, iced::window::Id::unique(), ) .is_none() ); assert!( captured_escape( key_event(iced::keyboard::key::Named::Enter), iced::event::Status::Captured, iced::window::Id::unique(), ) .is_none() ); }