fix(desktop): reject stale route owners before drag retirement
This commit is contained in:
@@ -1363,6 +1363,15 @@ impl App {
|
|||||||
{
|
{
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
// Selection can change without advancing the captured canvas context.
|
||||||
|
if let Message::Route(
|
||||||
|
crate::route_edit::Event::Commit(captured, changed, ..)
|
||||||
|
| crate::route_edit::Event::CommitAttachment(captured, changed, ..),
|
||||||
|
) = &message
|
||||||
|
&& (self.route_context() != Some(*captured) || changed.edge != self.selected)
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
if let Message::DiagramSelection(event)
|
if let Message::DiagramSelection(event)
|
||||||
| Message::StateDiagram(crate::behavior::state_edit::Event::Selection(event)) = &message
|
| Message::StateDiagram(crate::behavior::state_edit::Event::Selection(event)) = &message
|
||||||
&& !self.multiselect_envelope(event)
|
&& !self.multiselect_envelope(event)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use crate::diagram_usage::Target as SelectionTarget;
|
|||||||
|
|
||||||
#[path = "group/connection_lifetime.rs"]
|
#[path = "group/connection_lifetime.rs"]
|
||||||
mod connection_lifetime;
|
mod connection_lifetime;
|
||||||
|
#[path = "group/route_lifetime.rs"]
|
||||||
|
mod route_lifetime;
|
||||||
|
|
||||||
struct Group {
|
struct Group {
|
||||||
f: Fixture,
|
f: Fixture,
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
//! Captured route commits cannot retire a newer Allocation input owner.
|
||||||
|
use super::*;
|
||||||
|
use crate::route_edit::{Context, Event as RouteEvent};
|
||||||
|
|
||||||
|
fn routed_group() -> (Group, Id) {
|
||||||
|
let mut g = group(Layer::Oa);
|
||||||
|
let mut project = g.f.app.project().clone();
|
||||||
|
let edge = project.relate(
|
||||||
|
RelationKind::Interaction,
|
||||||
|
"Reviewed exchange",
|
||||||
|
g.f.work,
|
||||||
|
g.y,
|
||||||
|
);
|
||||||
|
project
|
||||||
|
.diagrams
|
||||||
|
.get_mut(&g.f.diagram)
|
||||||
|
.unwrap()
|
||||||
|
.routes
|
||||||
|
.push(EdgeRoute {
|
||||||
|
edge,
|
||||||
|
waypoints: vec![xy(700., 400.)],
|
||||||
|
label: Some(EdgeLabel {
|
||||||
|
fraction: 0.4,
|
||||||
|
offset: xy(5., 10.),
|
||||||
|
}),
|
||||||
|
source_attachment: None,
|
||||||
|
target_attachment: Some(EdgeAttachment {
|
||||||
|
side: PortAnchorSide::Left,
|
||||||
|
offset: 0.6,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
validate_candidate(&project).unwrap();
|
||||||
|
g.f.app = App::with_project(project);
|
||||||
|
g.f.app.active_diagram = Some(g.f.diagram);
|
||||||
|
g.f.app.zoom = 1.;
|
||||||
|
g.f.app.pan = Vector::ZERO;
|
||||||
|
g.f.app.fit_to_view = false;
|
||||||
|
choose(&mut g.f.app, SelectionTarget::Edge(edge), Choice::Replace);
|
||||||
|
assert_eq!(g.f.app.selected, edge);
|
||||||
|
let view = project_view(g.f.app.project(), g.f.diagram);
|
||||||
|
assert!(view.edges.iter().any(|e| e.id == edge));
|
||||||
|
assert_eq!(
|
||||||
|
crate::route_edit::attachment_node(
|
||||||
|
g.f.app.project(),
|
||||||
|
&g.f.app.project().diagrams[&g.f.diagram],
|
||||||
|
&view,
|
||||||
|
edge,
|
||||||
|
EdgeEnd::Source,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.id,
|
||||||
|
g.f.node
|
||||||
|
);
|
||||||
|
(g, edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packet(g: &Group, edge: Id, captured: Context, attachment: bool) -> RouteEvent {
|
||||||
|
let mut route =
|
||||||
|
crate::route_edit::stored_route(&g.f.app.project().diagrams[&g.f.diagram], edge);
|
||||||
|
if attachment {
|
||||||
|
route.source_attachment = Some(EdgeAttachment {
|
||||||
|
side: PortAnchorSide::Top,
|
||||||
|
offset: 0.25,
|
||||||
|
});
|
||||||
|
RouteEvent::CommitAttachment(captured, route, EdgeEnd::Source, g.f.node)
|
||||||
|
} else {
|
||||||
|
route.waypoints[0] = xy(750., 450.);
|
||||||
|
RouteEvent::Commit(captured, route, Some(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_route_noop(app: &mut App, event: RouteEvent) {
|
||||||
|
let document = app.document.snapshot();
|
||||||
|
let selected = selection(app);
|
||||||
|
let context = app.route_context();
|
||||||
|
let camera = (app.pan, app.zoom, app.canvas_epoch, app.camera_epoch);
|
||||||
|
let ownership = (app.allocation_drag.epoch, app.allocation_drag.arrange_next);
|
||||||
|
let press = app.allocation_drag.active.as_ref().map(|a| a.press.clone());
|
||||||
|
let preview = app.allocation_drag.preview.clone();
|
||||||
|
let feedback = app.allocation_drag.feedback.as_ref().map(|f| {
|
||||||
|
(
|
||||||
|
f.press.token,
|
||||||
|
f.text.clone(),
|
||||||
|
f.refused,
|
||||||
|
f.targets.clone(),
|
||||||
|
f.inspection_targets.clone(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let handles = (app.selected_bend, app.selected_attachment);
|
||||||
|
let interaction = app.source_symbols.interaction;
|
||||||
|
let status = app.status.clone();
|
||||||
|
let _ = app.update(Message::Route(event));
|
||||||
|
assert_eq!(selection(app), selected);
|
||||||
|
assert_eq!(app.route_context(), context);
|
||||||
|
assert_eq!(
|
||||||
|
(app.pan, app.zoom, app.canvas_epoch, app.camera_epoch),
|
||||||
|
camera
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(app.allocation_drag.epoch, app.allocation_drag.arrange_next),
|
||||||
|
ownership
|
||||||
|
);
|
||||||
|
match (press, app.allocation_drag.active.as_ref()) {
|
||||||
|
(Some(before), Some(after)) => assert!(Arc::ptr_eq(&before, &after.press)),
|
||||||
|
(None, None) => {}
|
||||||
|
_ => panic!("stale route packet changed the active Allocation token"),
|
||||||
|
}
|
||||||
|
match (preview, app.allocation_drag.preview.as_ref()) {
|
||||||
|
(Some(before), Some(after)) => assert!(Arc::ptr_eq(&before, after)),
|
||||||
|
(None, None) => {}
|
||||||
|
_ => panic!("stale route packet changed the Allocation preview"),
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
app.allocation_drag.feedback.as_ref().map(|f| (
|
||||||
|
f.press.token,
|
||||||
|
f.text.clone(),
|
||||||
|
f.refused,
|
||||||
|
f.targets.clone(),
|
||||||
|
f.inspection_targets.clone(),
|
||||||
|
)),
|
||||||
|
feedback
|
||||||
|
);
|
||||||
|
assert_eq!((app.selected_bend, app.selected_attachment), handles);
|
||||||
|
assert_eq!(app.source_symbols.interaction, interaction);
|
||||||
|
assert_eq!(app.status, status);
|
||||||
|
assert!(Arc::ptr_eq(&document, &app.document.snapshot()));
|
||||||
|
assert_eq!(app.source_workspace.bundle(), document.bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stale_route_preserves_new_group(attachment: bool, escape: bool) {
|
||||||
|
let (mut g, edge) = routed_group();
|
||||||
|
let old = g.f.app.route_context().unwrap();
|
||||||
|
let delayed = packet(&g, edge, old, attachment);
|
||||||
|
if escape {
|
||||||
|
let _ = g.f.app.update(Message::Escape);
|
||||||
|
assert_ne!(g.f.app.route_context(), Some(old));
|
||||||
|
}
|
||||||
|
select(&mut g.f.app, &[g.an, g.yn]);
|
||||||
|
let _ = g.f.app.update(Message::WorkDrag(Event::ArrangeNext));
|
||||||
|
assert!(g.f.app.allocation_drag.arrange_next);
|
||||||
|
if !escape {
|
||||||
|
assert_eq!(g.f.app.route_context(), Some(old));
|
||||||
|
assert_ne!(g.f.app.selected, edge);
|
||||||
|
}
|
||||||
|
assert_route_noop(&mut g.f.app, delayed.clone());
|
||||||
|
let before = g.f.app.project().clone();
|
||||||
|
let source = g.f.app.document.bundle().clone();
|
||||||
|
let selected = selection(&g.f.app);
|
||||||
|
let from = node_point(&g.f.app, g.an);
|
||||||
|
let to = from + Vector::new(400., 0.);
|
||||||
|
let mut state = CanvasState::default();
|
||||||
|
let press = begin(&mut g.f.app, &mut state, from);
|
||||||
|
assert_eq!(press.mode, Mode::Arrange);
|
||||||
|
move_pointer(&mut g.f.app, &mut state, to);
|
||||||
|
assert!(g.f.app.allocation_drag.preview.is_some());
|
||||||
|
if !escape {
|
||||||
|
assert_eq!(g.f.app.route_context(), Some(old));
|
||||||
|
assert_ne!(g.f.app.selected, edge);
|
||||||
|
}
|
||||||
|
assert_route_noop(&mut g.f.app, delayed);
|
||||||
|
release(&mut g.f.app, &mut state, to);
|
||||||
|
let mut expected = before.clone();
|
||||||
|
let diagram = expected.diagrams.get_mut(&g.f.diagram).unwrap();
|
||||||
|
diagram.nodes.get_mut(&g.an).unwrap().position = xy(500., 100.);
|
||||||
|
diagram.nodes.get_mut(&g.f.node).unwrap().position = xy(540., 200.);
|
||||||
|
diagram.nodes.get_mut(&g.yn).unwrap().position = xy(540., 600.);
|
||||||
|
// Both endpoint placements move by (400, 0); the saved pin follows once,
|
||||||
|
// while label offset and endpoint attachment settings stay unchanged.
|
||||||
|
diagram
|
||||||
|
.routes
|
||||||
|
.iter_mut()
|
||||||
|
.find(|r| r.edge == edge)
|
||||||
|
.unwrap()
|
||||||
|
.waypoints = vec![xy(1100., 400.)];
|
||||||
|
assert_eq!(selection(&g.f.app), selected);
|
||||||
|
exact_history(&mut g.f.app, &before, &source, &expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_route_commit_preserves_arrange_arm_and_live_group() {
|
||||||
|
stale_route_preserves_new_group(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_route_attachment_preserves_arrange_arm_and_live_group() {
|
||||||
|
stale_route_preserves_new_group(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_context_route_commit_after_selection_preserves_arrange_arm_and_live_group() {
|
||||||
|
stale_route_preserves_new_group(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_context_route_attachment_after_selection_preserves_arrange_arm_and_live_group() {
|
||||||
|
stale_route_preserves_new_group(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn captured_route_context_fields_independently_reject_before_retirement() {
|
||||||
|
for attachment in [false, true] {
|
||||||
|
let (mut g, edge) = routed_group();
|
||||||
|
let current = g.f.app.route_context().unwrap();
|
||||||
|
// The selected edge and route payload are valid. Each capture mismatch
|
||||||
|
// is isolated; no nonexistent route session can mask admission.
|
||||||
|
for captured in [
|
||||||
|
Context {
|
||||||
|
document: Id::new_v4(),
|
||||||
|
..current
|
||||||
|
},
|
||||||
|
Context {
|
||||||
|
diagram: Id::new_v4(),
|
||||||
|
..current
|
||||||
|
},
|
||||||
|
Context {
|
||||||
|
revision: current.revision + 1,
|
||||||
|
..current
|
||||||
|
},
|
||||||
|
Context {
|
||||||
|
epoch: current.epoch + 1,
|
||||||
|
..current
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
let event = packet(&g, edge, captured, attachment);
|
||||||
|
assert_route_noop(&mut g.f.app, event);
|
||||||
|
}
|
||||||
|
// Hold the captured packet fixed and advance the live context instead.
|
||||||
|
let event = packet(&g, edge, current, attachment);
|
||||||
|
g.f.app.canvas_epoch += 1;
|
||||||
|
assert_route_noop(&mut g.f.app, event.clone());
|
||||||
|
g.f.app.canvas_epoch = current.epoch;
|
||||||
|
g.f.app.active_diagram = None;
|
||||||
|
assert_route_noop(&mut g.f.app, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_route_commits_without_session_preserve_exact_route_history() {
|
||||||
|
for attachment in [false, true] {
|
||||||
|
let (mut g, edge) = routed_group();
|
||||||
|
let current = g.f.app.route_context().unwrap();
|
||||||
|
let event = packet(&g, edge, current, attachment);
|
||||||
|
let before = g.f.app.project().clone();
|
||||||
|
let source = g.f.app.document.bundle().clone();
|
||||||
|
let revision = g.f.app.document.revision();
|
||||||
|
assert!(g.f.app.connection.is_none());
|
||||||
|
assert!(g.f.app.selected_attachment.is_none());
|
||||||
|
let _ = g.f.app.update(Message::Route(event));
|
||||||
|
assert_eq!(g.f.app.document.revision(), revision + 1);
|
||||||
|
assert_eq!(g.f.app.selected, edge);
|
||||||
|
assert_eq!(
|
||||||
|
g.f.app.selected_bend,
|
||||||
|
if attachment { None } else { Some((edge, 0)) }
|
||||||
|
);
|
||||||
|
let mut expected = before.clone();
|
||||||
|
let route = expected
|
||||||
|
.diagrams
|
||||||
|
.get_mut(&g.f.diagram)
|
||||||
|
.unwrap()
|
||||||
|
.routes
|
||||||
|
.iter_mut()
|
||||||
|
.find(|r| r.edge == edge)
|
||||||
|
.unwrap();
|
||||||
|
if attachment {
|
||||||
|
route.source_attachment = Some(EdgeAttachment {
|
||||||
|
side: PortAnchorSide::Top,
|
||||||
|
offset: 0.25,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
route.waypoints = vec![xy(750., 450.)];
|
||||||
|
}
|
||||||
|
exact_history(&mut g.f.app, &before, &source, &expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user