//! Process selection through the real App admission, document and source boundary. use super::*; use crate::behavior::topology::group_reorder::Destination; use crate::behavior::topology::{self, CanvasEvent, CanvasTool}; use oa_core::OccurrenceEndpoint; struct Fixture { app: App, owner: Id, nodes: [Id; 5], link: Id, work: Id, other_owner: Id, } fn local(id: Id) -> Target { Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![], occurrence: id, }) } fn add(project: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id { let mut element = project .make_element( Kind::Occurrence, project.elements[&owner].layer(), name, Some(owner), ) .unwrap(); let Some(SharedBody::Occurrence { body: value, .. }) = element.shared_mut() else { unreachable!() }; *value = body; let id = element.id; *project = execute_batch(project, &[Command::CreateElement(element)]).unwrap(); id } fn open(project: Project, owner: Id) -> App { let mut app = App::with_project(project); let _ = app.update(Message::Select(owner)); let _ = app.update(Message::Page(Page::Behavior)); app.reconcile_multiselect(); assert_eq!(app.topology_context().unwrap().owner, owner); assert_eq!( app.multiselect_context().unwrap().scope.surface, Surface::Process(owner) ); app } fn fixture(layer: Layer) -> Fixture { let (f, _) = topology::direct_tests::empty_exchange_fixture(layer); let mut project = f.p; let mut nodes = vec![]; for name in ["a", "u", "b", "v", "c"] { nodes.push(add( &mut project, f.owner, name, OccurrenceBody::Action { action: Some(f.work[0]), lifeline: None, }, )); } let nodes: [Id; 5] = nodes.try_into().unwrap(); let [a, u, b, v, _] = nodes; let link = project.relate(RelationKind::SequenceLink, "a to u", a, u); project.relate(RelationKind::SequenceLink, "b to v", b, v); let other = project .make_element( if layer == Layer::Oa { Kind::Process } else { Kind::FunctionalChain }, layer, "Other owner", None, ) .unwrap(); let other_owner = other.id; project = execute_batch(&project, &[Command::CreateElement(other)]).unwrap(); validate_candidate(&project).unwrap(); Fixture { app: open(project, f.owner), owner: f.owner, nodes, link, work: f.work[0], other_owner, } } fn send(app: &mut App, event: Event) { let _ = app.update(Message::DiagramSelection(event)); } fn choose(app: &mut App, target: Target, choice: Choice) { let context = app.multiselect_context().unwrap(); send(app, Event::Choose(context, target, choice)); } fn pair(f: &mut Fixture) { choose(&mut f.app, local(f.nodes[4]), Choice::Replace); choose(&mut f.app, local(f.nodes[0]), Choice::Add); assert_eq!( f.app.multiselect_snapshot().unwrap().targets(), &[local(f.nodes[4]), local(f.nodes[0])] ); } fn source_matches(app: &App) { let compiled = source_project::compile_bundle(app.document.bundle(), LIMITS).unwrap(); assert_eq!(&compiled.decoded.project, app.project()); assert_eq!(&compiled.bundle, app.document.bundle()); assert_eq!( source_workspace::reachable(&app.source_workspace.bundle()).unwrap(), *app.document.bundle() ); } fn marker(app: &mut App, owner: Id, occurrence: Id) { use crate::diagram_usage as usage; let target = usage::collect( app.project(), occurrence, &app.behavior.topology, &app.behavior.scenario, ) .into_iter() .find(|u| u.surface == Surface::Process(owner) && u.target == local(occurrence)) .unwrap(); let context = usage::Context { document: app.canvas_document, revision: app.document.revision(), subject: occurrence, }; app.pending_diagram_reveal = Some((context, target.clone(), app.canvas_epoch)); app.diagram_navigation_focus = Some(usage::Focus { context, usage: target, selected: app.selected, }); assert!(app.diagram_focus(Surface::Process(owner)).is_some()); } #[test] fn completed_text_apply_rejects_captured_process_reorder_without_retiring_current_state() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); pair(&mut f); let stale = f.app.multiselect_snapshot().unwrap(); let before = f.app.document.snapshot(); assert!(!f.app.document.can_undo()); let element = f.nodes[0]; let location = f.app.document.compiled().index.declarations[&element].clone(); let original = &before.bundle.files[&location.path]; let quoted_name = format!("\"{}\"", f.app.project().elements[&element].name); assert_eq!(original.matches(quoted_name.as_str()).count(), 1); let edited = original.replace(quoted_name.as_str(), "\"Changed through Text\""); let mut expected = before.decoded.project.clone(); expected.elements.get_mut(&element).unwrap().name = "Changed through Text".into(); let mut expected_bundle = before.bundle.clone(); expected_bundle .files .insert(location.path.clone(), edited.clone()); let _ = f.app.update(Message::Page(Page::Text)); let _ = f.app.update(Message::SourceFile(location.path)); let _ = f .app .update(Message::SourceEdit(text_editor::Action::SelectAll)); let _ = f.app.update(Message::SourceEdit(text_editor::Action::Edit( text_editor::Edit::Paste(Arc::new(edited.clone())), ))); assert!(f.app.source_dirty()); assert_eq!(f.app.source_editor.text(), edited); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); let _ = f.app.update(Message::ApplySource); assert!(!f.app.source_dirty(), "{}", f.app.status); assert_eq!(f.app.project(), &expected); assert_eq!(f.app.document.bundle(), &expected_bundle); source_matches(&f.app); let _ = f.app.update(Message::Page(Page::Behavior)); assert_eq!(f.app.topology_context().unwrap().owner, f.owner); pair(&mut f); let current = f.app.multiselect_snapshot().unwrap(); assert_eq!(current.targets(), stale.targets()); assert_eq!(current.primary(), stale.primary()); assert_ne!(current.context(), stale.context()); let destination = Destination::After(f.nodes[2]); assert!( matches!( Prepared::new(f.app.project(), f.owner, current.targets()) .unwrap() .plan(destination) .unwrap(), Plan::Change(_) ), "the replay must be a real reorder, not a no-op" ); assert!( f.app .multiselect_envelope(&Event::ProcessReorder(current.clone(), destination)) ); let accepted = f.app.document.snapshot(); let revision = f.app.document.revision(); let workspace = f.app.source_workspace.bundle(); let editor = f.app.source_editor.text(); let selected = ( f.app.selected, f.app.selected_node, f.app.selected_bend, f.app.selected_attachment, ); let camera = (f.app.zoom, f.app.pan, f.app.fit_to_view, f.app.canvas_size); let process_camera = f.app.behavior.topology.camera_boundary(); let epochs = ( f.app.canvas_epoch, f.app.camera_epoch, f.app.allocation_drag.epoch, ); let draft = f .app .draft .as_ref() .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()); let form = f.app.behavior.recovery_snapshot(); let status = f.app.status.clone(); let pending_reveal = f.app.pending_diagram_reveal.clone(); send(&mut f.app, Event::ProcessReorder(stale, destination)); assert!(Arc::ptr_eq(&accepted, &f.app.document.snapshot())); assert_eq!(f.app.project(), &expected); assert_eq!(f.app.document.bundle(), &expected_bundle); assert_eq!(f.app.document.revision(), revision); assert_eq!(f.app.source_workspace.bundle(), workspace); assert_eq!(f.app.source_editor.text(), editor); assert_eq!(f.app.multiselect_snapshot(), Some(current)); assert_eq!( ( f.app.selected, f.app.selected_node, f.app.selected_bend, f.app.selected_attachment ), selected ); assert_eq!( (f.app.zoom, f.app.pan, f.app.fit_to_view, f.app.canvas_size), camera ); assert!(f.app.behavior.topology.camera_boundary() == process_camera); assert_eq!( ( f.app.canvas_epoch, f.app.camera_epoch, f.app.allocation_drag.epoch ), epochs ); assert_eq!( f.app .draft .as_ref() .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()), draft ); assert_eq!(f.app.behavior.recovery_snapshot(), form); assert_eq!(f.app.pending_diagram_reveal, pending_reveal); assert_eq!(f.app.status, status); assert!(!f.app.source_dirty()); assert!(f.app.document.can_undo()); assert!(!f.app.document.can_redo()); let _ = f.app.update(Message::Undo); assert_eq!(f.app.project(), &before.decoded.project); assert_eq!(f.app.document.bundle(), &before.bundle); assert!(!f.app.document.can_undo()); assert!(f.app.document.can_redo()); source_matches(&f.app); let _ = f.app.update(Message::Redo); assert_eq!(f.app.project(), &expected); assert_eq!(f.app.document.bundle(), &expected_bundle); assert!(f.app.document.can_undo()); assert!(!f.app.document.can_redo()); source_matches(&f.app); } } #[test] fn process_group_reverse_click_order_changes_only_owner_steps_and_undoes_once() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let [a, u, b, v, c] = f.nodes; pair(&mut f); let before = f.app.document.snapshot(); let selection = f.app.multiselect_snapshot().unwrap(); assert_eq!(selection.primary(), Some(&local(a))); send( &mut f.app, Event::ProcessReorder(selection.clone(), Destination::After(b)), ); let mut expected = before.decoded.project.clone(); *expected .elements .get_mut(&f.owner) .unwrap() .steps_mut() .unwrap() = vec![b, u, a, v, c]; assert_eq!(f.app.project(), &expected); assert_eq!(f.app.selected, a); let after_selection = f.app.multiselect_snapshot().unwrap(); assert_eq!(after_selection.targets(), selection.targets()); assert_eq!(after_selection.primary(), selection.primary()); source_matches(&f.app); let after = f.app.document.snapshot(); let reopened = Document::from_bundle(after.bundle.clone(), LIMITS).unwrap(); assert_eq!(reopened.project(), &expected); let _ = f.app.update(Message::Undo); assert_eq!(f.app.project(), &before.decoded.project); assert_eq!(f.app.document.bundle(), &before.bundle); assert!(!f.app.document.can_undo()); source_matches(&f.app); let _ = f.app.update(Message::Redo); assert_eq!(f.app.project(), &expected); assert_eq!(f.app.document.bundle(), &after.bundle); assert!(!f.app.document.can_redo()); source_matches(&f.app); } } #[test] fn process_noop_group_and_legacy_reorder_preserve_history_camera_and_pending_reveal() { for group in [true, false] { let mut f = fixture(Layer::Oa); if group { pair(&mut f); } else { choose(&mut f.app, local(f.nodes[0]), Choice::Replace); } marker(&mut f.app, f.owner, f.nodes[0]); let before = f.app.document.snapshot(); let selection = f.app.multiselect_snapshot().unwrap(); let pending = f.app.pending_diagram_reveal.clone(); let camera = f.app.behavior.topology.camera_boundary(); let epochs = (f.app.canvas_epoch, f.app.camera_epoch); f.app.status = "Unchanged order feedback".into(); let message = if group { Message::DiagramSelection(Event::ProcessReorder( selection.clone(), Destination::Before(f.nodes[4]), )) } else { Message::TopologyCanvas(CanvasEvent::Reorder { context: f.app.topology_context().unwrap(), occurrence: f.nodes[0], before: f.nodes[2], }) }; let _ = f.app.update(message); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert_eq!(f.app.multiselect_snapshot().unwrap(), selection); assert_eq!(f.app.pending_diagram_reveal, pending); assert!(f.app.behavior.topology.camera_boundary() == camera); assert_eq!((f.app.canvas_epoch, f.app.camera_epoch), epochs); assert_eq!(f.app.status, "Unchanged order feedback"); assert!(!f.app.document.can_undo()); assert!(!f.app.document.can_redo()); } } #[test] fn process_mixed_link_and_cross_column_groups_refuse_whole_operation() { for layer in [Layer::Oa, Layer::Sa] { for mixed in [true, false] { let mut f = fixture(layer); choose(&mut f.app, local(f.nodes[0]), Choice::Replace); let other = if mixed { Target::ProcessLink(f.link) } else { local(f.nodes[1]) }; choose(&mut f.app, other.clone(), Choice::Add); let selection = f.app.multiselect_snapshot().unwrap(); assert_eq!(selection.targets(), &[local(f.nodes[0]), other]); let before = f.app.document.snapshot(); send( &mut f.app, Event::ProcessReorder(selection.clone(), Destination::After(f.nodes[2])), ); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert_eq!(f.app.multiselect_snapshot().unwrap(), selection); assert!(!f.app.document.can_undo()); source_matches(&f.app); } } } #[test] fn process_stale_document_revision_owner_epoch_and_selection_aba_reject_before_shared_hooks() { for stale_by in ["document", "revision", "owner", "epoch", "ABA"] { let mut f = fixture(Layer::Oa); pair(&mut f); let stale = f.app.multiselect_snapshot().unwrap(); match stale_by { "document" => f.app.canvas_document = Id::new_v4(), "revision" => { f.app .document .execute(Command::Describe(f.work, "New revision".into())) .unwrap(); } "owner" => { f.app.behavior.active_owner = Some(f.other_owner); f.app.selected = f.other_owner; f.app.reconcile_multiselect(); } "epoch" => f.app.canvas_epoch += 1, _ => { choose(&mut f.app, local(f.nodes[0]), Choice::Toggle); choose(&mut f.app, local(f.nodes[0]), Choice::Toggle); assert_eq!( f.app.multiselect_snapshot().unwrap().targets(), stale.targets() ); assert_eq!( f.app.multiselect_snapshot().unwrap().primary(), stale.primary() ); } } let before = f.app.document.snapshot(); let selection = f.app.multiselect_snapshot(); let camera = f.app.behavior.topology.camera_boundary(); let selected = f.app.selected; let epochs = ( f.app.canvas_epoch, f.app.camera_epoch, f.app.allocation_drag.epoch, ); let history = (f.app.document.can_undo(), f.app.document.can_redo()); f.app.status = "Current owner feedback".into(); f.app.allocation_drag.arrange_next = true; send( &mut f.app, Event::ProcessReorder(stale, Destination::After(f.nodes[2])), ); assert!( Arc::ptr_eq(&before, &f.app.document.snapshot()), "{stale_by}" ); assert_eq!(f.app.multiselect_snapshot(), selection); assert_eq!(f.app.selected, selected); assert!(f.app.behavior.topology.camera_boundary() == camera); assert_eq!( ( f.app.canvas_epoch, f.app.camera_epoch, f.app.allocation_drag.epoch ), epochs ); assert_eq!( (f.app.document.can_undo(), f.app.document.can_redo()), history ); assert_eq!(f.app.status, "Current owner feedback"); assert!(f.app.allocation_drag.arrange_next); } } #[test] fn process_pending_properties_source_form_tool_and_busy_never_implicitly_apply_or_change_group() { for pending in [ "properties", "invalid properties", "source", "form", "tool", "busy", ] { let mut f = fixture(Layer::Oa); pair(&mut f); match pending { "properties" => { let _ = f.app.update(Message::Field( "/name".into(), "Unapplied contribution".into(), )); } "invalid properties" => { let _ = f .app .update(Message::Field("/alias".into(), "invalid alias!".into())); } "source" => { let text = format!("{}\n// Unapplied source\n", f.app.source_workspace.text()); let _ = f .app .update(Message::SourceEdit(text_editor::Action::SelectAll)); let _ = f.app.update(Message::SourceEdit(text_editor::Action::Edit( text_editor::Edit::Paste(Arc::new(text)), ))); } "form" => { let context = f.app.topology_context().unwrap(); let _ = f.app.update(Message::TopologyCanvas(CanvasEvent::Begin { context, tool: topology::Tool::Work, control: ControlKind::Choice, insertion: topology::Insertion::default(), })); let _ = f.app.update(Message::Behavior(behavior::Event::Topology( topology::Event::Name("Unsubmitted form".into()), ))); assert!(f.app.behavior.has_pending()); } "tool" => f.app.behavior.topology.choose(CanvasTool::Connect), _ => f.app.busy = true, } if pending == "source" { assert!(f.app.source_dirty()); } if pending.contains("properties") { assert!(f.app.draft_dirty()); } let selection = f.app.multiselect_snapshot().unwrap(); let before = f.app.document.snapshot(); let source = f.app.source_workspace.bundle(); let draft = f .app .draft .as_ref() .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()); let form = f.app.behavior.recovery_snapshot(); for event in [ Event::ProcessReorder(selection.clone(), Destination::After(f.nodes[2])), Event::Clear(selection.context()), Event::Choose(selection.context(), local(f.nodes[2]), Choice::Replace), ] { send(&mut f.app, event); assert!( Arc::ptr_eq(&before, &f.app.document.snapshot()), "{pending}" ); assert_eq!(f.app.source_workspace.bundle(), source); assert_eq!( f.app .draft .as_ref() .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()), draft ); assert_eq!(f.app.behavior.recovery_snapshot(), form); assert_eq!(f.app.multiselect_snapshot().unwrap(), selection); assert!(!f.app.document.can_undo()); } } } #[test] fn process_queued_singleton_selection_remove_reorder_and_reconnect_cannot_retire_current_group() { let mut f = fixture(Layer::Oa); pair(&mut f); marker(&mut f.app, f.owner, f.nodes[0]); let context = f.app.topology_context().unwrap(); let before = f.app.document.snapshot(); let selection = f.app.multiselect_snapshot().unwrap(); let pending = f.app.pending_diagram_reveal.clone(); let camera = f.app.behavior.topology.camera_boundary(); let epochs = ( f.app.canvas_epoch, f.app.camera_epoch, f.app.allocation_drag.epoch, ); f.app.status = "Group owns input".into(); for event in [ CanvasEvent::Select(context, f.nodes[2]), CanvasEvent::SelectQualified( context, OccurrenceEndpoint { references: vec![], occurrence: f.nodes[2], }, ), CanvasEvent::Remove(context, f.nodes[0]), CanvasEvent::Reorder { context, occurrence: f.nodes[0], before: f.nodes[4], }, CanvasEvent::Reconnect { context, link: f.link, end: topology::End::Target, target: f.nodes[4], }, ] { let _ = f.app.update(Message::TopologyCanvas(event)); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert_eq!(f.app.multiselect_snapshot().unwrap(), selection); assert_eq!(f.app.pending_diagram_reveal, pending); assert!(f.app.behavior.topology.camera_boundary() == camera); assert_eq!( ( f.app.canvas_epoch, f.app.camera_epoch, f.app.allocation_drag.epoch ), epochs ); assert_eq!(f.app.status, "Group owns input"); assert!(!f.app.delete_pending); assert!(!f.app.document.can_undo()); } } fn qualified_fixture() -> (Fixture, Id, Id, Id, Id) { let mut f = fixture(Layer::Oa); let mut project = f.app.project().clone(); let sequence = project .make_element(Kind::Process, Layer::Oa, "Reusable checks", None) .unwrap(); let sequence_id = sequence.id; project = execute_batch(&project, &[Command::CreateElement(sequence)]).unwrap(); // Collapsed reuse cards still show four preview entries. Keep the tested // leaf beyond that bound so collapsing really removes its exact target. for index in 0..4 { add( &mut project, sequence_id, &format!("Preview prefix {index}"), OccurrenceBody::Action { action: Some(f.work), lifeline: None, }, ); } let leaf = add( &mut project, sequence_id, "Shared check", OccurrenceBody::Action { action: Some(f.work), lifeline: None, }, ); let first = add( &mut project, f.owner, "First call", OccurrenceBody::SequenceReference { sequence: Some(sequence_id), }, ); let second = add( &mut project, f.owner, "Second call", OccurrenceBody::SequenceReference { sequence: Some(sequence_id), }, ); f.app = open(project, f.owner); let p = f.app.project().clone(); assert!(f.app.behavior.topology.expand(&p, f.owner, first)); assert!(f.app.behavior.topology.expand(&p, f.owner, second)); (f, sequence_id, first, second, leaf) } #[test] fn process_qualified_primary_is_exact_readonly_and_clears_without_semantic_rebinding() { let (mut f, _, first, second, leaf) = qualified_fixture(); let p = f.app.project().clone(); let endpoint = OccurrenceEndpoint { references: vec![first], occurrence: leaf, }; let target = Target::ProcessOccurrence(endpoint.clone()); assert!(f.app.multiselect_target_visible(&target)); choose(&mut f.app, target.clone(), Choice::Replace); assert_eq!( f.app.selected, leaf, "Inspect exact leaf, not the first caller or shared work" ); assert_eq!(f.app.process_preview_primary(), Some(&target)); assert_eq!( f.app .behavior .topology .selection_address() .unwrap() .endpoint, endpoint ); assert!(f.app.draft.is_none()); let before = f.app.document.snapshot(); for message in [ Message::Field("/name".into(), "Forbidden definition rename".into()), Message::Apply, Message::Delete, ] { let _ = f.app.update(message); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert!(f.app.draft.is_none()); assert!(!f.app.delete_pending); assert_eq!(f.app.process_preview_primary(), Some(&target)); } choose(&mut f.app, local(f.nodes[0]), Choice::Add); let mixed = f.app.multiselect_snapshot().unwrap(); send( &mut f.app, Event::ProcessReorder(mixed.clone(), Destination::After(f.nodes[2])), ); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert_eq!(f.app.multiselect_snapshot().unwrap(), mixed); choose(&mut f.app, target.clone(), Choice::Replace); let current = f.app.multiselect_context().unwrap(); send(&mut f.app, Event::Clear(current)); assert!(f.app.multiselect_snapshot().unwrap().targets().is_empty()); assert_eq!(f.app.selected, f.owner); assert!(f.app.behavior.topology.selection_address().is_none()); choose(&mut f.app, target, Choice::Replace); assert!(f.app.behavior.topology.expand(&p, f.owner, first)); f.app.reconcile_multiselect(); assert!(f.app.multiselect_snapshot().unwrap().targets().is_empty()); assert_eq!(f.app.selected, f.owner); assert!(f.app.behavior.topology.selection_address().is_none()); assert!(f.app.process_preview_primary().is_none()); assert!( f.app .multiselect_target_visible(&Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![second], occurrence: leaf })) ); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); } fn menu(app: &mut App, target: Target, label: &str) -> Message { use crate::context_menu::Entry; use crate::model_menu; let object = match &target { Target::ProcessOccurrence(endpoint) => endpoint.occurrence, _ => panic!("This fixture opens contribution menus"), }; let mut request = model_menu::Request::object(object, iced::Point::new(180., 150.)); request.process = app.topology_context().map(Box::new); request.selection_target = Some(Box::new(target)); let _ = app.update(Message::Menu(model_menu::Event::Open(request))); let opened = app .model_menu .as_ref() .expect("Captured Process menu must survive the open epoch change"); assert!(opened.valid(app)); assert_eq!( opened.request.process.as_deref().copied(), app.topology_context() ); model_menu::popup(app) .unwrap() .entries .into_iter() .find_map(|entry| match entry { Entry::Action { label: name, message: Some(message), .. } if name == label => Some(message), _ => None, }) .unwrap_or_else(|| panic!("Missing enabled menu action: {label}")) } #[test] fn process_menu_add_uses_rebound_epoch_and_preserves_existing_primary_membership() { let mut f = fixture(Layer::Oa); choose(&mut f.app, local(f.nodes[0]), Choice::Replace); let before = f.app.document.snapshot(); let old_context = f.app.topology_context().unwrap(); let action = menu(&mut f.app, local(f.nodes[4]), "Add to selection"); assert_ne!(f.app.topology_context().unwrap().epoch, old_context.epoch); assert_eq!(f.app.selected, f.nodes[0]); let _ = f.app.update(action); let selection = f.app.multiselect_snapshot().unwrap(); assert_eq!(selection.targets(), &[local(f.nodes[0]), local(f.nodes[4])]); assert_eq!(selection.primary(), Some(&local(f.nodes[4]))); assert_eq!(f.app.selected, f.nodes[4]); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert!(!f.app.document.can_undo()); } #[test] fn process_qualified_nonprimary_menu_opens_definition_from_caller_and_back_returns() { let (mut f, definition, first, _, leaf) = qualified_fixture(); choose(&mut f.app, local(f.nodes[0]), Choice::Replace); let before = f.app.document.snapshot(); let target = Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![first], occurrence: leaf, }); let action = menu(&mut f.app, target, "Open referenced definition"); assert_eq!(f.app.selected, f.nodes[0]); assert_eq!(f.app.topology_context().unwrap().owner, f.owner); let _ = f.app.update(action); assert_eq!(f.app.behavior.active_owner, Some(definition)); assert_eq!(f.app.topology_context().unwrap().owner, definition); let context = f.app.topology_context().unwrap(); let _ = f .app .update(Message::TopologyCanvas(CanvasEvent::Back(context))); assert_eq!(f.app.behavior.active_owner, Some(f.owner)); assert_eq!(f.app.topology_context().unwrap().owner, f.owner); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert!(!f.app.document.can_undo()); } #[test] fn process_qualified_definition_back_restores_exact_invocation_readonly_subject_and_camera() { for open_nonprimary in [false, true] { let (mut f, definition, first, second, leaf) = qualified_fixture(); // The shared fixture has already expanded both fifth-tile invocations. let context = f.app.topology_context().unwrap(); let _ = f.app.update(Message::TopologyCanvas(CanvasEvent::Zoom( context, Some(0.75), ))); let first_target = Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![first], occurrence: leaf, }); let second_target = Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![second], occurrence: leaf, }); if open_nonprimary { // Keep the requested second path outside the current selection so // its ordinary definition action is available in its context menu. choose(&mut f.app, local(f.nodes[0]), Choice::Replace); choose(&mut f.app, first_target.clone(), Choice::Add); } else { choose(&mut f.app, first_target.clone(), Choice::Replace); choose(&mut f.app, second_target.clone(), Choice::Add); } assert_eq!(f.app.multiselect_snapshot().unwrap().targets().len(), 2); assert_eq!( f.app.multiselect_snapshot().unwrap().primary(), Some(if open_nonprimary { &first_target } else { &second_target }) ); assert_eq!(f.app.selected, leaf); assert!(f.app.draft.is_none()); let original = f.app.document.snapshot(); let source = f.app.document.bundle().clone(); let camera = f.app.behavior.topology.camera_boundary(); let target = second_target; let action = if open_nonprimary { menu(&mut f.app, target.clone(), "Open referenced definition") } else { let Target::ProcessOccurrence(endpoint) = &target else { unreachable!() }; Message::TopologyCanvas(CanvasEvent::OpenQualified( f.app.topology_context().unwrap(), endpoint.clone(), )) }; let _ = f.app.update(action); assert_eq!(f.app.topology_context().unwrap().owner, definition); assert_eq!(f.app.selected, leaf); assert!(f.app.process_preview_primary().is_none()); assert!(f.app.draft.is_some(), "Definition view is editable"); let context = f.app.topology_context().unwrap(); let _ = f .app .update(Message::TopologyCanvas(CanvasEvent::Back(context))); assert_eq!(f.app.topology_context().unwrap().owner, f.owner); assert_eq!(f.app.behavior.active_owner, Some(f.owner)); assert_eq!( f.app.multiselect_snapshot().unwrap().targets(), std::slice::from_ref(&target) ); assert_eq!( f.app.multiselect_snapshot().unwrap().primary(), Some(&target) ); assert_eq!( f.app.selected, leaf, "Back must not inspect the outer reference" ); assert_eq!(f.app.process_preview_primary(), Some(&target)); let Target::ProcessOccurrence(endpoint) = &target else { unreachable!() }; let address = f.app.behavior.topology.selection_address().unwrap(); assert_eq!(address.owner, f.owner); assert_eq!(&address.endpoint, endpoint); assert!( f.app.multiselect_target_visible(&target), "Caller expansions survive Back" ); assert!( f.app.draft.is_none(), "Returned qualified inspector stays read-only" ); assert!(f.app.behavior.topology.camera_boundary() == camera); let _ = f.app.update(Message::Field( "/name".into(), "Must not edit the outer reference or definition".into(), )); assert!(f.app.draft.is_none()); assert_eq!(f.app.process_preview_primary(), Some(&target)); assert!(Arc::ptr_eq(&original, &f.app.document.snapshot())); assert_eq!(f.app.document.bundle(), &source); assert!(!f.app.document.can_undo() && !f.app.document.can_redo()); source_matches(&f.app); } } #[test] fn process_open_current_qualified_primary_and_catalog_navigation_restore_editable_draft() { for catalog in [false, true] { let (mut f, definition, first, _, leaf) = qualified_fixture(); let target = Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![first], occurrence: leaf, }); choose(&mut f.app, target.clone(), Choice::Replace); assert_eq!(f.app.selected, leaf); assert!(f.app.draft.is_none()); let before = f.app.document.snapshot(); if catalog { let _ = f.app.update(Message::Page(Page::Catalog)); assert_eq!(f.app.page, Page::Catalog); } else { let action = menu(&mut f.app, target, "Open referenced definition"); let _ = f.app.update(action); assert_eq!(f.app.topology_context().unwrap().owner, definition); } assert_eq!(f.app.selected, leaf); assert!(f.app.process_preview_primary().is_none()); assert!( f.app.draft.is_some(), "Leaving a read-only presentation restores normal property editing" ); let _ = f.app.update(Message::Field( "/name".into(), "Definition edit stays unapplied".into(), )); assert!(f.app.draft_dirty()); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert!(!f.app.document.can_undo()); } } #[test] fn process_explicit_object_navigation_leaves_qualified_use_even_for_same_leaf_id() { for edit_properties in [false, true] { for defining_leaf in [false, true] { let (mut f, definition, first, _, leaf) = qualified_fixture(); let qualified = Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![first], occurrence: leaf, }); choose(&mut f.app, qualified.clone(), Choice::Replace); assert_eq!(f.app.selected, leaf); assert_eq!(f.app.process_preview_primary(), Some(&qualified)); assert!(f.app.draft.is_none()); let before = f.app.document.snapshot(); let source = f.app.document.bundle().clone(); let camera = f.app.behavior.topology.camera_boundary(); let target = if defining_leaf { leaf } else { first }; let expected_owner = if defining_leaf { definition } else { f.owner }; let message = if edit_properties { Message::EditProperties(target) } else { Message::Select(target) }; let _ = f.app.update(message); assert_eq!(f.app.selected, target); assert_eq!(f.app.topology_context().unwrap().owner, expected_owner); assert_eq!(f.app.behavior.active_owner, Some(expected_owner)); assert_eq!( f.app.multiselect_snapshot().unwrap().targets(), &[local(target)] ); assert_eq!( f.app.multiselect_snapshot().unwrap().primary(), Some(&local(target)) ); assert!(f.app.behavior.topology.selection_address().is_none()); assert!(f.app.process_preview_primary().is_none()); assert!( f.app.draft.is_some(), "Explicit model-object navigation restores its editor" ); assert!(!f.app.draft_dirty()); if !defining_leaf { assert!(f.app.behavior.topology.camera_boundary() == camera); } assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert_eq!(f.app.document.bundle(), &source); assert!(!f.app.document.can_undo() && !f.app.document.can_redo()); source_matches(&f.app); } } } #[test] fn process_open_local_reference_after_qualified_inspection_returns_local_reference() { let (mut f, definition, first, _, leaf) = qualified_fixture(); choose( &mut f.app, Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![first], occurrence: leaf, }), Choice::Replace, ); let before = f.app.document.snapshot(); let source = f.app.document.bundle().clone(); let camera = f.app.behavior.topology.camera_boundary(); let context = f.app.topology_context().unwrap(); let _ = f .app .update(Message::TopologyCanvas(CanvasEvent::Open(context, first))); assert_eq!(f.app.topology_context().unwrap().owner, definition); assert_eq!(f.app.selected, definition); assert!(f.app.behavior.topology.selection_address().is_none()); let context = f.app.topology_context().unwrap(); let _ = f .app .update(Message::TopologyCanvas(CanvasEvent::Back(context))); assert_eq!(f.app.topology_context().unwrap().owner, f.owner); assert_eq!(f.app.selected, first); assert_eq!( f.app.multiselect_snapshot().unwrap().targets(), &[local(first)] ); assert!(f.app.behavior.topology.selection_address().is_none()); assert!(f.app.process_preview_primary().is_none()); assert!(f.app.draft.is_some()); assert!(f.app.behavior.topology.camera_boundary() == camera); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert_eq!(f.app.document.bundle(), &source); assert!(!f.app.document.can_undo() && !f.app.document.can_redo()); source_matches(&f.app); } #[test] fn process_usage_navigation_between_two_preview_paths_keeps_exact_link_identity() { use crate::diagram_usage as usage; let (mut f, definition, first, second, leaf) = qualified_fixture(); let mut project = f.app.project().clone(); let next = add( &mut project, definition, "Following check", OccurrenceBody::Action { action: Some(f.work), lifeline: None, }, ); let relation = project.relate(RelationKind::SequenceLink, "Reusable path", leaf, next); f.app = open(project, f.owner); let project = f.app.project().clone(); assert!(f.app.behavior.topology.expand(&project, f.owner, first)); assert!(f.app.behavior.topology.expand(&project, f.owner, second)); let before = f.app.document.snapshot(); for call in [first, second] { let target = Target::ProcessPreviewLink { relation, source: OccurrenceEndpoint { references: vec![call], occurrence: leaf, }, target: OccurrenceEndpoint { references: vec![call], occurrence: next, }, }; let context = usage::Context { document: f.app.canvas_document, revision: f.app.document.revision(), subject: relation, }; let _ = f .app .update(Message::DiagramUsage(usage::Event::Open(context))); let token = f.app.diagram_usages.as_ref().unwrap().token; let rows = usage::collect( f.app.project(), relation, &f.app.behavior.topology, &f.app.behavior.scenario, ); let row = rows .iter() .find(|row| row.surface == Surface::Process(f.owner) && row.target == target) .unwrap() .clone(); let _ = f.app.update(Message::DiagramUsagesReady(usage::Completion { token, context, result: Ok(rows), })); let _ = f .app .update(Message::DiagramUsage(usage::Event::Choose(token, row))); assert_eq!(f.app.selected, relation); assert_eq!( f.app.multiselect_snapshot().unwrap().primary(), Some(&target) ); assert_eq!(f.app.process_preview_primary(), Some(&target)); assert!(f.app.draft.is_none()); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); } }