Preserve exact Process selection across definition navigation

Keep caller identity and property subjects aligned when opening and returning from qualified contributions, and when explicitly navigating to local references. Validate camera, source, history and read-only boundaries.
This commit is contained in:
2026-09-23 06:25:50 -07:00
parent b15345123a
commit ff712b2419
4 changed files with 231 additions and 7 deletions
@@ -672,6 +672,111 @@ fn process_qualified_nonprimary_menu_opens_definition_from_caller_and_back_retur
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] {
@@ -708,6 +813,100 @@ fn process_open_current_qualified_primary_and_catalog_navigation_restore_editabl
}
}
#[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;