//! App transaction and draft boundaries for captured state-canvas messages. use super::*; use crate::behavior::{ self, state_edit::{self, Context, Endpoint, Event, Tool}, states, }; struct Fixture { app: App, machine: Id, region: Id, a: Id, b: Id, leaf: Id, members: BTreeSet, } fn send(app: &mut App, message: Message) { // Exercise the real App boundary without polling asynchronous I/O tasks. let _ = app.update(message); } fn event(app: &mut App, event: Event) { send(app, Message::StateDiagram(event)); } fn form(app: &mut App, event: states::Event) { send(app, Message::Behavior(behavior::Event::States(event))); } fn fixture() -> Fixture { let mut project = Project::empty(); let mut add = |kind, name, parent| project.add_element(kind, Layer::Sa, name, parent).unwrap(); let owner = add(Kind::System, "System", None); let machine = add(Kind::StateMachine, "Operating modes", Some(owner)); let region = add(Kind::Region, "Main", Some(machine)); let initial = add(Kind::Initial, "Start", Some(region)); let a = add(Kind::State, "Operating", Some(region)); let b = add(Kind::State, "Standby", Some(region)); let nested = add(Kind::Region, "Work", Some(a)); let leaf = add(Kind::State, "Working", Some(nested)); let other_machine = add(Kind::StateMachine, "Other machine", None); let other_region = add(Kind::Region, "Other region", Some(other_machine)); add(Kind::State, "Foreign state", Some(other_region)); let mut app = App::with_project(project); send(&mut app, Message::Select(a)); send(&mut app, Message::Page(Page::Behavior)); assert_eq!(behavior::selected_owner(&app), Some(machine)); Fixture { app, machine, region, a, b, leaf, members: BTreeSet::from([machine, region, initial, a, b, nested, leaf]), } } fn create_view(f: &mut Fixture) -> Context { event(&mut f.app, Event::CreateView(f.machine)); f.app .state_context() .expect("CreateView must activate the new persisted view") } fn node(app: &App, diagram: Id, element: Id) -> Id { app.project().diagrams[&diagram] .nodes .values() .find(|n| n.element == element) .unwrap() .id } fn assert_source_model_agree(app: &App) { assert_eq!( source_project::compile_bundle(app.document.bundle(), LIMITS) .unwrap() .decoded .project, *app.project() ); assert_eq!( source_workspace::reachable(&app.source_workspace.bundle()).unwrap(), *app.document.bundle() ); assert!(!app.source_dirty()); } #[test] fn state_diagram_routes_constructor_and_source_open_to_the_existing_workspace_without_edits() { let mut f = fixture(); let diagram = create_view(&mut f).diagram; let project = f.app.project().clone(); let app = App::with_project(project.clone()); assert_eq!(app.page, Page::Behavior); assert_eq!(app.state_context().unwrap().diagram, diagram); assert_eq!(behavior::selected_owner(&app), Some(f.machine)); assert_eq!(app.behavior.diagram.tool, Tool::Select); assert_eq!(app.selected, project.id); assert_eq!(app.project(), &project); assert!(!app.document.can_undo()); let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("state-view.arcadia"); let text = format!( "// Exact authored source must remain unchanged.\n{}", language::format(&project) ); std::fs::write(&path, &text).unwrap(); let mut reopened = App::with_project(Project::empty()); send( &mut reopened, Message::Loaded(Ok(Some(read_snapshot(path.clone()).unwrap()))), ); assert_eq!(reopened.page, Page::Behavior); assert_eq!(reopened.state_context().unwrap().diagram, diagram); assert_eq!(reopened.project(), &project); assert_eq!( reopened.document.bundle().files[&reopened.document.bundle().entry], text ); assert_eq!(std::fs::read_to_string(path).unwrap(), text); assert!(!reopened.document.can_undo()); assert_source_model_agree(&reopened); } #[test] fn state_diagram_routes_picker_and_reveal_but_explicit_architecture_does_not_bounce() { let mut f = fixture(); let diagram = create_view(&mut f).diagram; let mut project = f.app.project().clone(); let second = state_edit::create_view(&project, f.machine).unwrap(); project = prepare_command(&project, &second.command).unwrap().project; let transition = project.relate(RelationKind::StateTransition, "Switch", f.a, f.b); let mut app = App::with_project(project.clone()); let before = app.document.snapshot(); send(&mut app, Message::Page(Page::Diagram)); assert_eq!(app.page, Page::Diagram); assert_eq!(app.active_diagram, None); send( &mut app, Message::Diagram(Choice { id: second.diagram, label: "Second state view".into(), }), ); assert_eq!(app.page, Page::Behavior); assert_eq!(app.state_context().unwrap().diagram, second.diagram); send(&mut app, Message::RevealInDiagram(diagram, transition)); assert_eq!(app.page, Page::Behavior); assert_eq!(app.state_context().unwrap().diagram, diagram); assert_eq!(app.selected, transition); assert_eq!(app.draft.as_ref().unwrap().original, transition); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); assert_source_model_agree(&app); let architecture = project.ensure_diagram(Layer::Sa); let mut app = App::with_project(project); send( &mut app, Message::Diagram(Choice { id: diagram, label: "State view".into(), }), ); assert_eq!(app.page, Page::Behavior); send(&mut app, Message::Page(Page::Diagram)); assert_eq!(app.page, Page::Diagram); assert_eq!(app.active_diagram, Some(architecture)); assert!(app.state_context().is_none()); assert!(!app.document.can_undo()); } #[test] fn state_diagram_routes_preserve_selected_raw_properties_and_pending_behavior_guards() { let mut f = fixture(); let diagram = create_view(&mut f).diagram; let mut app = App::with_project(f.app.project().clone()); send(&mut app, Message::Select(f.a)); send( &mut app, Message::Field("/alias".into(), "invalid alias!".into()), ); let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); assert!(!app.apply()); let before = app.document.snapshot(); // Exercise normalization itself with an existing raw draft, including callers // that have just adopted a view without a successful property Apply. app.page = Page::Diagram; app.route_state_diagram_workspace(); assert_eq!(app.page, Page::Behavior); assert_eq!(app.selected, f.a); assert_eq!( serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), raw ); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); send( &mut app, Message::Diagram(Choice { id: diagram, label: "State view".into(), }), ); assert_eq!( serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), raw ); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); let mut app = App::with_project(f.app.project().clone()); form( &mut app, states::Event::Start(f.machine, states::Tool::Transition, f.a), ); form( &mut app, states::Event::Name("Pending behavior stays".into()), ); let pending = app.behavior.recovery_snapshot().unwrap(); let context = app.state_context().unwrap(); send(&mut app, Message::Page(Page::Diagram)); send( &mut app, Message::Diagram(Choice { id: diagram, label: "State view".into(), }), ); assert_eq!(app.page, Page::Behavior); assert_eq!(app.state_context().unwrap().diagram, context.diagram); assert_eq!(app.behavior.recovery_snapshot().unwrap(), pending); assert!(!app.document.can_undo()); } struct NavigationFixture { app: App, machine_a: Id, diagram_a: Id, state_a: Id, transition_a: Id, root_event: Id, machine_b: Id, diagram_b: Id, state_b: Id, } fn navigation_fixture(layer_b: Layer) -> NavigationFixture { let mut project = Project::empty(); let mut machine = |layer, name| { let machine = project .add_element(Kind::StateMachine, layer, name, None) .unwrap(); let region = project .add_element(Kind::Region, layer, "Main", Some(machine)) .unwrap(); let state = project .add_element(Kind::State, layer, "Ready", Some(region)) .unwrap(); (machine, state) }; let (machine_a, state_a) = machine(Layer::Sa, "Machine A"); let (machine_b, state_b) = machine(layer_b, "Machine B"); let root_event = project .add_element(Kind::Event, Layer::Sa, "External event", None) .unwrap(); let transition_a = project.relate(RelationKind::StateTransition, "Repeat", state_a, state_a); let plan_a = state_edit::create_view(&project, machine_a).unwrap(); project = prepare_command(&project, &plan_a.command).unwrap().project; let plan_b = state_edit::create_view(&project, machine_b).unwrap(); project = prepare_command(&project, &plan_b.command).unwrap().project; let mut app = App::with_project(project); send(&mut app, Message::Select(state_a)); send(&mut app, Message::Page(Page::Behavior)); send( &mut app, Message::BehaviorDiagram(Choice { id: plan_a.diagram, label: "A view".into(), }), ); let context = app.state_context().unwrap(); event( &mut app, Event::Viewport(context, iced::Size::new(820., 540.)), ); event(&mut app, Event::Zoom(context, 0.65)); event( &mut app, Event::Pan(context, iced::Vector::new(-1_400., -800.)), ); event( &mut app, Event::Choose(Tool::Reconnect { relation: transition_a, endpoint: Endpoint::Target, }), ); assert_eq!(app.state_context().unwrap().diagram, plan_a.diagram); assert!(!app.behavior.diagram.fit); NavigationFixture { app, machine_a, diagram_a: plan_a.diagram, state_a, transition_a, root_event, machine_b, diagram_b: plan_b.diagram, state_b, } } fn assert_switched_state_view(f: &mut NavigationFixture) { assert_eq!(behavior::selected_owner(&f.app), Some(f.machine_b)); assert_eq!(f.app.state_context().unwrap().diagram, f.diagram_b); let defaults = behavior::state_view::State::default(); let state = &f.app.behavior.diagram; assert_eq!(state.diagram, Some(f.diagram_b)); assert_eq!( state.tool, Tool::Select, "Reconnect from A must not be armed in B" ); assert_eq!(state.pan, defaults.pan); assert_eq!(state.zoom, defaults.zoom); assert!(state.fit); assert!( state.viewport.is_none(), "B needs its own first viewport measurement" ); assert!(state.placement.is_none()); let context = f.app.state_context().unwrap(); event( &mut f.app, Event::Viewport(context, iced::Size::new(820., 540.)), ); assert!( f.app.behavior.diagram.pan != defaults.pan || f.app.behavior.diagram.zoom != defaults.zoom, "The unchanged widget size still fits a newly selected view" ); } fn catalog_selects_another_machine(layer_b: Layer) { let mut f = navigation_fixture(layer_b); let before = f.app.document.snapshot(); send(&mut f.app, Message::Page(Page::Catalog)); send(&mut f.app, Message::Select(f.state_b)); assert_eq!(f.app.layer, layer_b); send(&mut f.app, Message::Page(Page::Behavior)); assert_switched_state_view(&mut f); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert_source_model_agree(&f.app); } #[test] fn state_navigation_catalog_selection_of_another_machine_resets_camera_and_tool() { catalog_selects_another_machine(Layer::Sa); } #[test] fn state_navigation_catalog_selection_across_layers_resets_camera_and_tool() { catalog_selects_another_machine(Layer::Oa); } #[test] fn state_navigation_layer_change_resets_the_new_machine_at_the_same_viewport_size() { let mut f = navigation_fixture(Layer::Oa); let before = f.app.document.snapshot(); send(&mut f.app, Message::Layer(Layer::Oa)); assert_switched_state_view(&mut f); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert_source_model_agree(&f.app); } #[test] fn state_navigation_return_to_the_same_view_preserves_camera_tool_and_fit_mode() { for fit in [false, true] { let mut f = navigation_fixture(Layer::Sa); if fit { let context = f.app.state_context().unwrap(); event(&mut f.app, Event::Fit(context)); } let state = &f.app.behavior.diagram; let retained = ( state.diagram, state.pan, state.zoom, state.tool, state.fit, state.viewport, ); let before = f.app.document.snapshot(); send(&mut f.app, Message::Page(Page::Catalog)); send(&mut f.app, Message::Select(f.state_a)); send(&mut f.app, Message::Page(Page::Behavior)); let context = f.app.state_context().unwrap(); event( &mut f.app, Event::Viewport(context, iced::Size::new(820., 540.)), ); assert_eq!(behavior::selected_owner(&f.app), Some(f.machine_a)); assert_eq!(context.diagram, f.diagram_a); let state = &f.app.behavior.diagram; assert_eq!( ( state.diagram, state.pan, state.zoom, state.tool, state.fit, state.viewport ), retained ); assert_eq!( state.tool, Tool::Reconnect { relation: f.transition_a, endpoint: Endpoint::Target } ); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); } } #[test] fn state_navigation_root_event_and_same_layer_keep_the_current_camera_and_tool() { let mut f = navigation_fixture(Layer::Sa); let state = &f.app.behavior.diagram; let retained = ( state.diagram, state.pan, state.zoom, state.tool, state.fit, state.viewport, ); let before = f.app.document.snapshot(); send(&mut f.app, Message::Select(f.root_event)); assert_eq!(f.app.selected, f.root_event); send(&mut f.app, Message::Page(Page::Catalog)); send(&mut f.app, Message::Page(Page::Behavior)); send(&mut f.app, Message::Layer(Layer::Sa)); assert_eq!(behavior::selected_owner(&f.app), Some(f.machine_a)); assert_eq!(f.app.state_context().unwrap().diagram, f.diagram_a); let state = &f.app.behavior.diagram; assert_eq!( ( state.diagram, state.pan, state.zoom, state.tool, state.fit, state.viewport ), retained ); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); } #[test] fn create_view_places_only_its_machine_and_restores_exact_source_in_one_undo() { let mut f = fixture(); let before = f.app.document.snapshot(); let context = create_view(&mut f); let app = &mut f.app; let diagram = &app.project().diagrams[&context.diagram]; assert_eq!( diagram.projection, ProjectionKind::StateMachine { machine: f.machine } ); assert_eq!( diagram .nodes .values() .map(|n| n.element) .collect::>(), f.members ); assert_eq!(app.project().elements, before.decoded.project.elements); assert_eq!(app.project().relations, before.decoded.project.relations); assert_eq!(app.selected, f.machine); assert!(app.selected_node.is_none()); assert_eq!(app.document.revision(), 1); assert_source_model_agree(app); let accepted = app.document.snapshot(); send(app, Message::Undo); assert_eq!(app.project(), &before.decoded.project); assert_eq!(app.document.bundle(), &before.bundle); assert!(!app.document.can_undo()); assert!(!app.dirty()); assert_source_model_agree(app); send(app, Message::Redo); assert_eq!(app.project(), &accepted.decoded.project); assert_eq!(app.document.bundle(), &accepted.bundle); assert_source_model_agree(app); } #[test] fn direct_create_connect_select_and_move_keep_semantic_identity_and_atomic_history() { let mut f = fixture(); let context = create_view(&mut f); let app = &mut f.app; let before_create = app.document.snapshot(); event( app, Event::CreateAt { context, owner: f.region, kind: Kind::State, position: Position { x: 900., y: 700. }, }, ); let created = app.selected; assert_eq!(app.project().elements[&created].parent, Some(f.region)); assert_eq!(app.project().elements[&created].kind(), Kind::State); assert_eq!( app.project().elements.len(), before_create.decoded.project.elements.len() + 1 ); let created_node = node(app, context.diagram, created); assert_ne!(created_node, created); assert_eq!( app.project().diagrams[&context.diagram].nodes[&created_node].position, Position { x: 900., y: 700. } ); assert_source_model_agree(app); let before_connect = app.document.snapshot(); let context = app.state_context().unwrap(); event( app, Event::Connect { context, source: f.a, target: created, }, ); let relation = app.selected; assert!(matches!(app.project().relations[&relation].body, RelationBody::StateTransition { source, target, transition_kind: TransitionKind::External, .. } if source == f.a && target == created)); assert!(app.selected_node.is_none()); assert_eq!(behavior::selected_owner(app), Some(f.machine)); assert_source_model_agree(app); let context = app.state_context().unwrap(); let before_select = app.document.snapshot(); event(app, Event::Select(context, f.a)); assert_eq!(app.selected, f.a); assert_eq!( app.state_context(), Some(context), "The press selection must not invalidate its following drag release" ); assert!(Arc::ptr_eq(&before_select, &app.document.snapshot())); let placement = node(app, context.diagram, f.a); let leaf = node(app, context.diagram, f.leaf); let original = app.project().diagrams[&context.diagram].nodes[&placement].position; let leaf_original = app.project().diagrams[&context.diagram].nodes[&leaf].position; event( app, Event::Move { context, node: placement, position: Position { x: original.x + 31., y: original.y + 43., }, }, ); assert_eq!(app.selected, f.a); assert!(app.selected_node.is_none()); assert_eq!( app.project().diagrams[&context.diagram].nodes[&leaf].position, Position { x: leaf_original.x + 31., y: leaf_original.y + 43. } ); assert_eq!( app.document.revision(), 4, "View, vertex, edge, and drag each commit once" ); assert_source_model_agree(app); for expected in [before_select, before_connect, before_create] { send(app, Message::Undo); assert_eq!(app.project(), &expected.decoded.project); assert_eq!(app.document.bundle(), &expected.bundle); assert_source_model_agree(app); } assert!(app.project().diagrams.contains_key(&context.diagram)); assert!(!app.project().contains(created)); assert!(!app.project().contains(relation)); } #[test] fn all_context_dimensions_reject_stale_selection_camera_and_model_messages() { let mut f = fixture(); let context = create_view(&mut f); let app = &mut f.app; let before = app.document.snapshot(); let selected = app.selected; let pan = app.behavior.diagram.pan; let zoom = app.behavior.diagram.zoom; let viewport = app.behavior.diagram.viewport; let placement = node(app, context.diagram, f.a); for changed in 0..4 { let mut captured = context; match changed { 0 => captured.document = Id::new_v4(), 1 => captured.diagram = Id::new_v4(), 2 => captured.revision += 1, _ => captured.epoch += 1, } for stale in [ Event::Select(captured, f.b), Event::Viewport(captured, iced::Size::new(999., 777.)), Event::Pan(captured, iced::Vector::new(55., 66.)), Event::Zoom(captured, 2.7), Event::Move { context: captured, node: placement, position: Position { x: 88., y: 99. }, }, Event::Connect { context: captured, source: f.a, target: f.b, }, Event::CreateAt { context: captured, owner: f.region, kind: Kind::State, position: Position { x: 800., y: 800. }, }, ] { event(app, stale); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); assert_eq!(app.selected, selected); assert_eq!(app.behavior.diagram.pan, pan); assert_eq!(app.behavior.diagram.zoom, zoom); assert_eq!(app.behavior.diagram.viewport, viewport); assert_eq!(app.state_context(), Some(context)); assert!(!app.behavior.has_pending()); } } assert_source_model_agree(app); } #[test] fn real_view_tool_and_history_changes_invalidate_old_release_events() { let mut f = fixture(); let first = create_view(&mut f); let second = create_view(&mut f); let app = &mut f.app; assert_ne!(first.diagram, second.diagram); let first_choice = Choice { id: first.diagram, label: app.project().named(first.diagram).into(), }; send(app, Message::BehaviorDiagram(first_choice)); let restored_view = app.state_context().unwrap(); assert_eq!(restored_view.diagram, first.diagram); let before = app.document.snapshot(); for captured in [first, second] { event( app, Event::Connect { context: captured, source: f.a, target: f.b, }, ); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); } event(app, Event::Choose(Tool::Transition)); let armed = app.state_context().unwrap(); assert_ne!(restored_view.epoch, armed.epoch); event( app, Event::Connect { context: restored_view, source: f.a, target: f.b, }, ); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); event( app, Event::Connect { context: armed, source: f.a, target: f.b, }, ); assert_eq!( app.project().relations.len(), before.decoded.project.relations.len() + 1 ); let committed = app.state_context().unwrap(); send(app, Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); let undone = app.document.snapshot(); event( app, Event::Connect { context: armed, source: f.a, target: f.b, }, ); event( app, Event::Connect { context: committed, source: f.a, target: f.b, }, ); assert!(Arc::ptr_eq(&undone, &app.document.snapshot())); send(app, Message::Page(Page::Diagram)); assert!(app.state_context().is_none()); let diagram_count = app.project().diagrams.len(); event(app, Event::CreateView(f.machine)); assert_eq!(app.project().diagrams.len(), diagram_count); assert!(Arc::ptr_eq(&undone, &app.document.snapshot())); } #[test] fn rejected_creation_keeps_source_and_selection_then_valid_owner_can_retry() { let mut f = fixture(); let context = create_view(&mut f); let app = &mut f.app; let before = app.document.snapshot(); let selected = app.selected; event( app, Event::CreateAt { context, owner: f.a, kind: Kind::State, position: Position { x: 600., y: 700. }, }, ); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); assert_eq!(app.selected, selected); assert_eq!(app.state_context(), Some(context)); event( app, Event::CreateAt { context, owner: f.region, kind: Kind::State, position: Position { x: 600., y: 700. }, }, ); assert_eq!(app.project().elements[&app.selected].parent, Some(f.region)); assert_eq!(app.document.revision(), context.revision + 1); assert_source_model_agree(app); send(app, Message::Undo); assert_eq!(app.project(), &before.decoded.project); assert_eq!(app.document.bundle(), &before.bundle); } #[test] fn pending_drafts_block_every_direct_mutation_and_passive_camera_preserves_inputs() { for pending in ["properties", "source", "behavior"] { let mut f = fixture(); let context = create_view(&mut f); event( &mut f.app, Event::Connect { context, source: f.a, target: f.b, }, ); let relation = f.app.selected; // A valid Place request must have a genuinely absent placement. let missing = f .app .project() .make_element(Kind::State, Layer::Sa, "Not yet displayed", Some(f.region)) .unwrap(); let missing_id = missing.id; send( &mut f.app, Message::Command(Command::CreateElement(missing)), ); send(&mut f.app, Message::Select(f.a)); let app = &mut f.app; let context = app.state_context().unwrap(); let placement = node(app, context.diagram, f.a); let at = app.project().diagrams[&context.diagram].nodes[&placement].position; let mutations = [ Event::CreateAt { context, owner: f.region, kind: Kind::State, position: Position { x: 700., y: 800. }, }, Event::Place { context, element: missing_id, position: Position { x: 800., y: 900. }, }, Event::Move { context, node: placement, position: Position { x: at.x + 50., y: at.y + 70., }, }, Event::Connect { context, source: f.b, target: f.a, }, Event::Reconnect { context, relation, endpoint: Endpoint::Target, vertex: f.leaf, }, ]; for mutation in &mutations { let plan = state_edit::plan(app.project(), context, mutation).unwrap(); assert_ne!( &prepare_command(app.project(), &plan.command) .unwrap() .project, app.project(), "Each blocked probe must otherwise be a valid, substantive model change" ); } match pending { "properties" => send(app, Message::Field("/name".into(), "Unapplied name".into())), "source" => { let draft = format!( "{}\n// Unapplied source note.\n", app.source_workspace.text() ); send(app, Message::SourceEdit(text_editor::Action::SelectAll)); send( app, Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste( Arc::new(draft), ))), ); } _ => { form(app, states::Event::Edit(relation)); form(app, states::Event::Name("Retained form name".into())); form(app, states::Event::Guard("temperature nominal".into())); form(app, states::Event::TriggerNotes("Retained trigger".into())); } } assert_eq!(app.draft_dirty(), pending == "properties"); assert_eq!(app.source_dirty(), pending == "source"); assert_eq!(app.behavior.has_pending(), pending == "behavior"); let before = app.document.snapshot(); let source = app.source_workspace.bundle(); let properties = app.draft.as_ref().unwrap().materialize().unwrap(); let pending_candidate = app .behavior .prepare_pending(app.project()) .unwrap() .map(|command| prepare_command(app.project(), &command).unwrap().project); let selected = app.selected; for mutation in mutations { event(app, mutation); assert!(Arc::ptr_eq(&before, &app.document.snapshot()), "{pending}"); assert_eq!(app.selected, selected); assert_eq!(app.source_workspace.bundle(), source); assert_eq!( app.draft.as_ref().unwrap().materialize().unwrap(), properties ); } for passive in [ Event::Viewport(context, iced::Size::new(960., 640.)), Event::Pan(context, iced::Vector::new(12., 18.)), Event::Zoom(context, 0.7), Event::Fit(context), ] { event(app, passive); assert!(Arc::ptr_eq(&before, &app.document.snapshot()), "{pending}"); assert_eq!(app.source_workspace.bundle(), source); assert_eq!( app.draft.as_ref().unwrap().materialize().unwrap(), properties ); } assert_eq!( app.behavior .prepare_pending(app.project()) .unwrap() .map(|command| prepare_command(app.project(), &command).unwrap().project), pending_candidate ); match pending { "properties" => send(app, Message::RevertProperties), "source" => send(app, Message::RevertSource), _ => form(app, states::Event::Cancel), } assert!(!app.draft_dirty() && !app.source_dirty() && !app.behavior.has_pending()); let context = app.state_context().unwrap(); event( app, Event::Move { context, node: placement, position: Position { x: at.x + 50., y: at.y + 70., }, }, ); assert_eq!(app.selected, f.a); assert_ne!(app.document.bundle(), &before.bundle); assert_source_model_agree(app); send(app, Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); assert_eq!(app.project(), &before.decoded.project); } } #[test] fn fallback_owner_first_selection_keeps_the_captured_context_for_its_first_move() { let f = fixture(); let mut project = f.app.project().clone(); // One eligible machine makes the fallback deterministic. Start from a loaded // architecture view with no selected behavior element or chosen owner. A // state-only project now opens its saved view with an explicit owner. project .elements .retain(|id, element| f.members.contains(id) || element.kind() == Kind::System); let plan = state_edit::create_view(&project, f.machine).unwrap(); let mut project = prepare_command(&project, &plan.command).unwrap().project; let architecture = project.ensure_diagram(Layer::Sa); project.diagrams.get_mut(&architecture).unwrap().projection = ProjectionKind::Allocation; let mut app = App::with_project(project); assert_eq!(app.selected, app.project().id); assert!(app.behavior.active_owner.is_none()); send(&mut app, Message::Page(Page::Behavior)); let context = app.state_context().unwrap(); assert_eq!(context.diagram, plan.diagram); assert_eq!(behavior::selected_owner(&app), Some(f.machine)); let before = app.document.snapshot(); let placement = node(&app, context.diagram, f.a); let original = app.project().diagrams[&context.diagram].nodes[&placement].position; event(&mut app, Event::Select(context, f.a)); assert_eq!(app.selected, f.a); assert_eq!( app.state_context(), Some(context), "Pinning the already-visible fallback owner must not advance the gesture epoch" ); assert_eq!(app.behavior.diagram.placement, Some(placement)); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); let destination = Position { x: original.x + 37., y: original.y + 29., }; event( &mut app, Event::Move { context, node: placement, position: destination, }, ); assert_eq!( app.project().diagrams[&context.diagram].nodes[&placement].position, destination ); assert_eq!(app.selected, f.a); assert_eq!( app.document.revision(), 1, "The first release must commit without another press" ); assert_source_model_agree(&app); send(&mut app, Message::Undo); assert_eq!(app.project(), &before.decoded.project); assert_eq!(app.document.bundle(), &before.bundle); assert!(!app.document.can_undo()); } #[test] fn same_size_viewport_after_revision_or_tool_epoch_does_not_refit_the_camera() { let mut f = fixture(); let context = create_view(&mut f); let app = &mut f.app; let size = iced::Size::new(960., 640.); event(app, Event::Viewport(context, size)); assert!(app.behavior.diagram.fit); let camera = (app.behavior.diagram.pan, app.behavior.diagram.zoom); let placement = node(app, context.diagram, f.a); // Move far enough to change projected bounds. Refitting now would visibly // jump the camera even though the canvas has not changed size. event( app, Event::Move { context, node: placement, position: Position { x: 4000., y: 3000. }, }, ); let changed = app.state_context().unwrap(); assert_ne!(changed.revision, context.revision); assert_eq!( (app.behavior.diagram.pan, app.behavior.diagram.zoom), camera ); assert!(app.behavior.diagram.fit); let after_move = app.document.snapshot(); event(app, Event::Viewport(changed, size)); assert_eq!( (app.behavior.diagram.pan, app.behavior.diagram.zoom), camera, "Context initialization is not a canvas resize" ); event(app, Event::Choose(Tool::Transition)); let changed_tool = app.state_context().unwrap(); assert_ne!(changed_tool.epoch, changed.epoch); event(app, Event::Viewport(changed_tool, size)); assert_eq!( (app.behavior.diagram.pan, app.behavior.diagram.zoom), camera ); assert!(Arc::ptr_eq(&after_move, &app.document.snapshot())); // A real resize still uses automatic fitting while fit mode is active. event( app, Event::Viewport(changed_tool, iced::Size::new(1200., 720.)), ); assert_ne!( (app.behavior.diagram.pan, app.behavior.diagram.zoom), camera ); assert!(Arc::ptr_eq(&after_move, &app.document.snapshot())); assert_source_model_agree(app); } #[test] fn shared_semantic_states_keep_architecture_and_behavior_placements_independent() { let f = fixture(); let mut project = f.app.project().clone(); let architecture = project.ensure_diagram(Layer::Sa); let arch_a = project.place(architecture, f.a, Position { x: 110., y: 90. }); let arch_b = project.place(architecture, f.b, Position { x: 780., y: 240. }); project.place(architecture, f.leaf, Position { x: 190., y: 310. }); let state_view = state_edit::create_view(&project, f.machine).unwrap(); let project = prepare_command(&project, &state_view.command) .unwrap() .project; let architecture_before = project.diagrams[&architecture].clone(); let mut app = App::with_project(project); let architecture_choice = Choice { id: architecture, label: app.project().named(architecture).into(), }; // The initial UUID-ordered view may be a state view, which correctly opens // Behavior. Enter Architecture explicitly before testing its selection. send(&mut app, Message::Page(Page::Diagram)); send(&mut app, Message::Diagram(architecture_choice)); assert_eq!(app.page, Page::Diagram); send(&mut app, Message::Select(f.b)); assert_eq!(app.active_diagram, Some(architecture)); assert_eq!(app.selected_node, Some(arch_b)); send(&mut app, Message::Page(Page::Behavior)); let context = app.state_context().unwrap(); assert_eq!(context.diagram, state_view.diagram); let state_a = node(&app, context.diagram, f.a); assert_ne!(state_a, arch_a); event(&mut app, Event::Select(context, f.a)); assert_eq!(app.selected, f.a); assert!( app.selected_node.is_none(), "Behavior must not borrow the architecture placement" ); assert_eq!(app.behavior.diagram.placement, Some(state_a)); assert_eq!(app.active_diagram, Some(architecture)); let before_move = app.document.snapshot(); let original = app.project().diagrams[&context.diagram].nodes[&state_a].position; let destination = Position { x: original.x + 41., y: original.y + 63., }; event( &mut app, Event::Move { context, node: state_a, position: destination, }, ); assert_eq!( app.project().diagrams[&context.diagram].nodes[&state_a].position, destination ); assert_eq!(app.project().diagrams[&architecture], architecture_before); assert_eq!(app.selected, f.a); assert!(app.selected_node.is_none()); assert_eq!(app.behavior.diagram.placement, Some(state_a)); assert_source_model_agree(&app); let moved = app.document.snapshot(); send(&mut app, Message::Page(Page::Diagram)); assert_eq!(app.active_diagram, Some(architecture)); assert_eq!(app.selected, f.a); assert_eq!( app.selected_node, Some(arch_a), "Returning to architecture resolves that view's placement of the same state" ); assert!(Arc::ptr_eq(&moved, &app.document.snapshot())); send(&mut app, Message::Page(Page::Behavior)); assert!(app.selected_node.is_none()); assert_eq!(app.behavior.diagram.placement, Some(state_a)); send(&mut app, Message::Undo); assert_eq!(app.project(), &before_move.decoded.project); assert_eq!(app.document.bundle(), &before_move.bundle); assert_eq!(app.project().diagrams[&architecture], architecture_before); assert_eq!(app.behavior.diagram.placement, Some(state_a)); assert!(!app.document.can_undo()); }