Add exact Process selection and atomic column-slot group reorder
Preserve the full-desktop-1 captured source. Qualified previews inspect their exact subject read-only; group movement preserves source order and other columns. Native acceptance and strict-lint payload-size corrections follow separately.
This commit is contained in:
@@ -0,0 +1,777 @@
|
||||
//! 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 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();
|
||||
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, 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_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_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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user