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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user