603 lines
22 KiB
Rust
603 lines
22 KiB
Rust
//! Saved-view group operations through the real App message boundary.
|
|
use super::multiselect::{Choice as GroupChoice, Event as GroupEvent};
|
|
use super::*;
|
|
use crate::diagram_usage::Target;
|
|
|
|
fn point(x: f32, y: f32) -> Position {
|
|
Position { x, y }
|
|
}
|
|
|
|
struct Fixture {
|
|
app: App,
|
|
diagram: Id,
|
|
a: Id,
|
|
b: Id,
|
|
child: Id,
|
|
edge: Id,
|
|
}
|
|
|
|
fn fixture() -> Fixture {
|
|
let mut p = Project::empty();
|
|
let a = p
|
|
.add_element(Kind::Function, Layer::Sa, "Send", None)
|
|
.unwrap();
|
|
let b = p
|
|
.add_element(Kind::Function, Layer::Sa, "Receive", None)
|
|
.unwrap();
|
|
let child = p
|
|
.add_element(Kind::Function, Layer::Sa, "Prepare", Some(a))
|
|
.unwrap();
|
|
let edge = p.relate(RelationKind::FunctionalExchange, "Signal", a, b);
|
|
let diagram = p.ensure_diagram(Layer::Sa);
|
|
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::FunctionalDataflow;
|
|
let a = p.place(diagram, a, point(10., 20.));
|
|
let b = p.place(diagram, b, point(500., 20.));
|
|
let child = p.place(diagram, child, point(40., 100.));
|
|
p.diagrams
|
|
.get_mut(&diagram)
|
|
.unwrap()
|
|
.routes
|
|
.push(EdgeRoute {
|
|
edge,
|
|
source_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Bottom,
|
|
offset: 0.3,
|
|
}),
|
|
target_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Top,
|
|
offset: 0.7,
|
|
}),
|
|
waypoints: vec![point(370., 150.)],
|
|
label: Some(EdgeLabel {
|
|
fraction: 0.4,
|
|
offset: point(5., -9.),
|
|
}),
|
|
});
|
|
let other = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Operational work", None)
|
|
.unwrap();
|
|
let other_diagram = p.ensure_diagram(Layer::Oa);
|
|
p.place(other_diagram, other, point(-50., -100.));
|
|
let mut app = App::with_project(p);
|
|
app.page = Page::Diagram;
|
|
app.layer = Layer::Sa;
|
|
app.active_diagram = Some(diagram);
|
|
app.selected = diagram;
|
|
app.selected_node = None;
|
|
app.refresh_draft();
|
|
app.reconcile_multiselect();
|
|
Fixture {
|
|
app,
|
|
diagram,
|
|
a,
|
|
b,
|
|
child,
|
|
edge,
|
|
}
|
|
}
|
|
|
|
fn dispatch(app: &mut App, event: GroupEvent) {
|
|
let _ = app.update(Message::DiagramSelection(event));
|
|
}
|
|
fn choose(app: &mut App, target: Target, choice: GroupChoice) {
|
|
let context = app.multiselect_context().unwrap();
|
|
dispatch(app, GroupEvent::Choose(context, target, choice));
|
|
}
|
|
fn select_pair(f: &mut Fixture) {
|
|
choose(&mut f.app, Target::Placement(f.a), GroupChoice::Replace);
|
|
choose(&mut f.app, Target::Placement(f.b), GroupChoice::Toggle);
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().targets(),
|
|
&[Target::Placement(f.a), Target::Placement(f.b)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn queued_singleton_move_or_delete_cannot_partially_edit_a_new_group() {
|
|
for model in [false, true] {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let before = f.app.document.snapshot();
|
|
let selected = f.app.multiselect_snapshot().unwrap();
|
|
let object = f.app.project().diagrams[&f.diagram].nodes[&f.b].element;
|
|
for message in [
|
|
Message::MoveNode(f.b, point(700., 600.)),
|
|
Message::DiagramRemove(super::diagram_removal::Intent {
|
|
context: f.app.route_context().unwrap(),
|
|
object,
|
|
placement: Some(f.b),
|
|
model,
|
|
}),
|
|
Message::Delete,
|
|
] {
|
|
let _ = f.app.update(message);
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().targets(),
|
|
selected.targets()
|
|
);
|
|
assert!(!f.app.delete_pending);
|
|
assert!(!f.app.document.can_undo());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_hide_action_applies_to_the_complete_visible_group() {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let before = f.app.document.snapshot();
|
|
let mut expected = f.app.project().clone();
|
|
let view = expected.diagrams.get_mut(&f.diagram).unwrap();
|
|
view.nodes.remove(&f.a);
|
|
view.nodes.remove(&f.b);
|
|
let _ = f.app.update(Message::HideSelected);
|
|
assert_eq!(f.app.project(), &expected);
|
|
assert_source_matches_model(&f.app);
|
|
let _ = f.app.update(Message::Undo);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert!(!f.app.document.can_undo());
|
|
}
|
|
fn assert_source_matches_model(app: &App) {
|
|
let compiled = oa_core::source_project::compile_bundle(app.document.bundle(), LIMITS).unwrap();
|
|
assert_eq!(&compiled.decoded.project, app.project());
|
|
assert_eq!(&compiled.bundle, app.document.bundle());
|
|
}
|
|
|
|
#[test]
|
|
fn toggled_group_moves_model_and_source_once_and_keeps_primary_through_history() {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let before = f.app.document.snapshot();
|
|
let snapshot = f.app.multiselect_snapshot().unwrap();
|
|
assert_eq!(snapshot.primary(), Some(&Target::Placement(f.b)));
|
|
let mut expected = f.app.project().clone();
|
|
let view = expected.diagrams.get_mut(&f.diagram).unwrap();
|
|
view.nodes.get_mut(&f.a).unwrap().position = point(50., 80.);
|
|
view.nodes.get_mut(&f.b).unwrap().position = point(540., 80.);
|
|
view.nodes.get_mut(&f.child).unwrap().position = point(80., 160.);
|
|
view.routes[0].waypoints = vec![point(410., 210.)];
|
|
dispatch(
|
|
&mut f.app,
|
|
GroupEvent::Move(snapshot.clone(), point(40., 60.)),
|
|
);
|
|
assert_eq!(f.app.project(), &expected);
|
|
assert_source_matches_model(&f.app);
|
|
assert_eq!(f.app.selected_node, Some(f.b));
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().targets(),
|
|
snapshot.targets()
|
|
);
|
|
let after = f.app.document.snapshot();
|
|
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());
|
|
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());
|
|
assert_eq!(f.app.selected_node, Some(f.b));
|
|
}
|
|
|
|
#[test]
|
|
fn ancestor_and_child_selection_moves_child_exactly_once() {
|
|
let mut f = fixture();
|
|
choose(&mut f.app, Target::Placement(f.a), GroupChoice::Replace);
|
|
choose(&mut f.app, Target::Placement(f.child), GroupChoice::Toggle);
|
|
let snapshot = f.app.multiselect_snapshot().unwrap();
|
|
assert_eq!(snapshot.targets().len(), 2);
|
|
let mut expected = f.app.project().clone();
|
|
let view = expected.diagrams.get_mut(&f.diagram).unwrap();
|
|
view.nodes.get_mut(&f.a).unwrap().position = point(0., 45.);
|
|
view.nodes.get_mut(&f.child).unwrap().position = point(30., 125.);
|
|
dispatch(&mut f.app, GroupEvent::Move(snapshot, point(-10., 25.)));
|
|
assert_eq!(f.app.project(), &expected);
|
|
assert_eq!(f.app.selected_node, Some(f.child));
|
|
assert_source_matches_model(&f.app);
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_edge_or_port_selection_refuses_the_entire_move() {
|
|
for include_port in [false, true] {
|
|
let mut f = fixture();
|
|
choose(&mut f.app, Target::Placement(f.a), GroupChoice::Replace);
|
|
let extra = if include_port {
|
|
let view = projection::project_view(f.app.project(), f.diagram);
|
|
let port = view
|
|
.ports
|
|
.iter()
|
|
.find(|p| p.owner == f.a)
|
|
.expect("functional exchange exposes its typed owner port");
|
|
Target::Port {
|
|
port: port.element,
|
|
placement: port.owner,
|
|
}
|
|
} else {
|
|
Target::Edge(f.edge)
|
|
};
|
|
choose(&mut f.app, extra.clone(), GroupChoice::Toggle);
|
|
let selection = f.app.multiselect_snapshot().unwrap();
|
|
assert_eq!(selection.targets(), &[Target::Placement(f.a), extra]);
|
|
let before = f.app.document.snapshot();
|
|
let revision = f.app.document.revision();
|
|
dispatch(
|
|
&mut f.app,
|
|
GroupEvent::Move(selection.clone(), point(40., 60.)),
|
|
);
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert_eq!(f.app.document.revision(), revision);
|
|
assert!(!f.app.document.can_undo());
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().targets(),
|
|
selection.targets()
|
|
);
|
|
assert!(f.app.status.contains("Move requires only placed elements"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stale_operation_envelopes_have_no_selection_history_status_or_allocation_side_effects() {
|
|
// Isolate each lifetime component, including the same visible set recreated
|
|
// by a remove/re-add cycle. Accepted setup changes are outside the assertion.
|
|
for stale_by in ["epoch", "revision", "document", "selection generation"] {
|
|
for action in ["move", "hide", "refuse", "collapse"] {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let stale = f.app.multiselect_snapshot().unwrap();
|
|
match stale_by {
|
|
"epoch" => f.app.canvas_epoch += 1,
|
|
"revision" => {
|
|
let semantic = f.app.project().diagrams[&f.diagram].nodes[&f.a].element;
|
|
// Advance only the document revision, retaining this canvas
|
|
// and selection epoch to independently test revision checking.
|
|
f.app
|
|
.document
|
|
.execute(Command::Describe(semantic, "New revision".into()))
|
|
.unwrap();
|
|
}
|
|
"document" => f.app.canvas_document = Id::new_v4(),
|
|
_ => {
|
|
choose(&mut f.app, Target::Placement(f.b), GroupChoice::Toggle);
|
|
choose(&mut f.app, Target::Placement(f.b), GroupChoice::Toggle);
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().targets(),
|
|
stale.targets()
|
|
);
|
|
}
|
|
}
|
|
f.app.status = "Current feedback must survive".into();
|
|
f.app.allocation_drag.arrange_next = true;
|
|
let before = f.app.document.snapshot();
|
|
let selection = f.app.multiselect_snapshot();
|
|
let legacy = (f.app.selected, f.app.selected_node);
|
|
let revision = f.app.document.revision();
|
|
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());
|
|
let draft = f
|
|
.app
|
|
.draft
|
|
.as_ref()
|
|
.map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap());
|
|
let event = match action {
|
|
"move" => GroupEvent::Move(stale, point(99., 100.)),
|
|
"hide" => GroupEvent::Hide(stale),
|
|
"collapse" => GroupEvent::Collapse(stale, Target::Placement(f.a)),
|
|
_ => GroupEvent::Refuse(stale, "Obsolete refusal".into()),
|
|
};
|
|
dispatch(&mut f.app, event);
|
|
assert_eq!(
|
|
f.app.project(),
|
|
&before.decoded.project,
|
|
"{stale_by}/{action}"
|
|
);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert_eq!(f.app.document.revision(), revision);
|
|
assert_eq!(f.app.multiselect_snapshot(), selection);
|
|
assert_eq!((f.app.selected, f.app.selected_node), legacy);
|
|
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 feedback must survive");
|
|
assert!(f.app.allocation_drag.arrange_next);
|
|
assert_eq!(
|
|
f.app
|
|
.draft
|
|
.as_ref()
|
|
.map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()),
|
|
draft
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pending_valid_and_invalid_property_drafts_refuse_group_edits_without_applying_them() {
|
|
for (path, value) in [("/name", "Unapplied name"), ("/id", "-")] {
|
|
for hide in [false, true] {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let _ = f.app.update(Message::Field(path.into(), value.into()));
|
|
assert!(f.app.draft_dirty());
|
|
let snapshot = f.app.multiselect_snapshot().unwrap();
|
|
let before = f.app.document.snapshot();
|
|
let draft =
|
|
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
|
let event = if hide {
|
|
GroupEvent::Hide(snapshot.clone())
|
|
} else {
|
|
GroupEvent::Move(snapshot.clone(), point(20., 30.))
|
|
};
|
|
dispatch(&mut f.app, event);
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert!(!f.app.document.can_undo());
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().targets(),
|
|
snapshot.targets()
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
|
draft
|
|
);
|
|
assert!(f.app.status.contains("Finish or cancel"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hiding_a_group_removes_only_exact_placements_and_roundtrips_in_one_history_step() {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let snapshot = f.app.multiselect_snapshot().unwrap();
|
|
let before = f.app.document.snapshot();
|
|
let mut expected = f.app.project().clone();
|
|
let view = expected.diagrams.get_mut(&f.diagram).unwrap();
|
|
view.nodes.remove(&f.a);
|
|
view.nodes.remove(&f.b);
|
|
dispatch(&mut f.app, GroupEvent::Hide(snapshot));
|
|
assert_eq!(
|
|
f.app.project(),
|
|
&expected,
|
|
"engineering objects, descendant placement, authored route and opposite-layer view survive"
|
|
);
|
|
assert!(f.app.multiselect_snapshot().unwrap().targets().is_empty());
|
|
assert_source_matches_model(&f.app);
|
|
let after = f.app.document.snapshot();
|
|
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());
|
|
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());
|
|
}
|
|
fn assert_surviving_primary(f: &Fixture) {
|
|
let selection = f.app.multiselect_snapshot().unwrap();
|
|
assert_eq!(selection.targets(), &[Target::Placement(f.a)]);
|
|
assert_eq!(selection.primary(), Some(&Target::Placement(f.a)));
|
|
assert_eq!(f.app.selected_node, Some(f.a));
|
|
assert_eq!(
|
|
f.app.selected,
|
|
f.app.project().diagrams[&f.diagram].nodes[&f.a].element
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn removing_primary_placement_from_model_preserves_exact_survivor_after_undo() {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let before = f.app.document.snapshot();
|
|
let _ = f.app.update(Message::Command(Command::Delete(f.b)));
|
|
assert!(
|
|
!f.app.project().diagrams[&f.diagram]
|
|
.nodes
|
|
.contains_key(&f.b)
|
|
);
|
|
assert_surviving_primary(&f);
|
|
let _ = f.app.update(Message::Undo);
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert_surviving_primary(&f);
|
|
let _ = f.app.update(Message::Redo);
|
|
assert_surviving_primary(&f);
|
|
}
|
|
|
|
#[test]
|
|
fn deleting_primary_model_object_then_undo_does_not_restore_removed_selection_membership() {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let before = f.app.document.snapshot();
|
|
let semantic = f.app.project().diagrams[&f.diagram].nodes[&f.b].element;
|
|
let _ = f.app.update(Message::Command(Command::Delete(semantic)));
|
|
assert!(!f.app.project().elements.contains_key(&semantic));
|
|
assert_surviving_primary(&f);
|
|
let _ = f.app.update(Message::Undo);
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert_surviving_primary(&f);
|
|
let _ = f.app.update(Message::Redo);
|
|
assert_surviving_primary(&f);
|
|
}
|
|
|
|
#[test]
|
|
fn replacing_primary_occurrence_with_same_semantic_placement_does_not_retarget_selection() {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let mut candidate = f.app.project().clone();
|
|
let view = candidate.diagrams.get_mut(&f.diagram).unwrap();
|
|
let mut replacement = view.nodes.remove(&f.b).unwrap();
|
|
let replacement_id = Id::new_v4();
|
|
replacement.id = replacement_id;
|
|
replacement.alias = "replacement_receive".into();
|
|
replacement.position = point(800., 300.);
|
|
view.nodes.insert(replacement_id, replacement);
|
|
// Replace preserves the semantic inspector identity. Its normal sync used
|
|
// to rediscover the new occurrence through that identity and steal the set.
|
|
let _ = f
|
|
.app
|
|
.update(Message::Command(Command::Replace(candidate.clone())));
|
|
assert_eq!(f.app.project(), &candidate);
|
|
assert_surviving_primary(&f);
|
|
assert!(
|
|
!f.app
|
|
.multiselect_snapshot()
|
|
.unwrap()
|
|
.targets()
|
|
.contains(&Target::Placement(replacement_id))
|
|
);
|
|
assert_source_matches_model(&f.app);
|
|
let _ = f.app.update(Message::Undo);
|
|
assert!(
|
|
f.app.project().diagrams[&f.diagram]
|
|
.nodes
|
|
.contains_key(&f.b)
|
|
);
|
|
assert_surviving_primary(&f);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_surface_navigation_and_return_never_resurrect_the_group() {
|
|
for destination in [Page::Catalog, Page::Behavior] {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let before = f.app.document.snapshot();
|
|
let stale = f.app.multiselect_snapshot().unwrap();
|
|
let _ = f.app.update(Message::Page(destination));
|
|
assert!(f.app.multiselect_snapshot().is_none());
|
|
let _ = f.app.update(Message::Page(Page::Diagram));
|
|
let live = f.app.multiselect_snapshot().unwrap();
|
|
assert!(
|
|
live.targets().len() <= 1,
|
|
"navigation may inspect one exact member but must not restore the group"
|
|
);
|
|
dispatch(&mut f.app, GroupEvent::Move(stale, point(400., 500.)));
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert!(!f.app.document.can_undo());
|
|
}
|
|
}
|
|
|
|
fn open_placement_menu(f: &mut Fixture, placement: Id) {
|
|
let semantic = f.app.project().diagrams[&f.diagram].nodes[&placement].element;
|
|
let mut request = crate::model_menu::Request::object(semantic, iced::Point::new(100., 100.));
|
|
request.placement = Some(placement);
|
|
request.canvas = f.app.route_context();
|
|
let _ = f
|
|
.app
|
|
.update(Message::Menu(crate::model_menu::Event::Open(request)));
|
|
assert!(f.app.model_menu.is_some());
|
|
}
|
|
|
|
fn menu_action(app: &App, caption: &str) -> Message {
|
|
crate::model_menu::popup(app)
|
|
.unwrap()
|
|
.entries
|
|
.into_iter()
|
|
.find_map(|entry| match entry {
|
|
crate::context_menu::Entry::Action { label, message, .. } if label == caption => {
|
|
message
|
|
}
|
|
_ => None,
|
|
})
|
|
.unwrap_or_else(|| panic!("enabled menu action {caption:?} must exist"))
|
|
}
|
|
|
|
#[test]
|
|
fn context_menu_dismissal_preserves_group_and_add_keeps_pre_menu_members() {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let before = f.app.document.snapshot();
|
|
let selected = f.app.multiselect_snapshot().unwrap();
|
|
let child = f.child;
|
|
open_placement_menu(&mut f, child);
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().targets(),
|
|
selected.targets()
|
|
);
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().primary(),
|
|
selected.primary()
|
|
);
|
|
let _ = f.app.update(Message::Menu(crate::model_menu::Event::Close));
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().targets(),
|
|
selected.targets()
|
|
);
|
|
assert_eq!(
|
|
f.app.multiselect_snapshot().unwrap().primary(),
|
|
selected.primary()
|
|
);
|
|
open_placement_menu(&mut f, child);
|
|
let add = menu_action(&f.app, "Add to selection");
|
|
let _ = f.app.update(add);
|
|
let selected = f.app.multiselect_snapshot().unwrap();
|
|
assert_eq!(
|
|
selected.targets(),
|
|
&[
|
|
Target::Placement(f.a),
|
|
Target::Placement(f.b),
|
|
Target::Placement(child)
|
|
]
|
|
);
|
|
assert_eq!(selected.primary(), Some(&Target::Placement(child)));
|
|
assert_eq!(f.app.project(), &before.decoded.project);
|
|
assert_eq!(f.app.document.bundle(), &before.bundle);
|
|
assert!(!f.app.document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn selected_member_context_menu_exposes_and_executes_group_hide_not_singleton_deletion() {
|
|
let mut f = fixture();
|
|
select_pair(&mut f);
|
|
let before = f.app.document.snapshot();
|
|
let member = f.a;
|
|
open_placement_menu(&mut f, member);
|
|
let popup = crate::model_menu::popup(&f.app).unwrap();
|
|
for entry in &popup.entries {
|
|
if let crate::context_menu::Entry::Action {
|
|
message: Some(Message::Menu(crate::model_menu::Event::Invoke(_, action))),
|
|
..
|
|
} = entry
|
|
{
|
|
assert!(
|
|
!matches!(
|
|
action.as_ref(),
|
|
Message::HideSelected | Message::Delete | Message::DiagramRemove(_)
|
|
),
|
|
"a selected-member group menu must not expose singleton removal"
|
|
);
|
|
}
|
|
}
|
|
let hide = menu_action(&f.app, "Hide selected placements");
|
|
let _ = f.app.update(hide);
|
|
let mut expected = before.decoded.project.clone();
|
|
let view = expected.diagrams.get_mut(&f.diagram).unwrap();
|
|
view.nodes.remove(&f.a);
|
|
view.nodes.remove(&f.b);
|
|
assert_eq!(f.app.project(), &expected);
|
|
assert!(!f.app.delete_pending);
|
|
assert!(f.app.model_menu.is_none());
|
|
assert_source_matches_model(&f.app);
|
|
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());
|
|
}
|