481 lines
15 KiB
Rust
481 lines
15 KiB
Rust
use super::*;
|
|
|
|
#[test]
|
|
fn state_bend_delayed_commit_after_same_view_selection_is_inert_and_live_commit_works() {
|
|
let (mut app, _, relation, _) = setup();
|
|
let context = app.state_context().unwrap();
|
|
let next = app.project().relations[&relation].target();
|
|
let waypoints = vec![Position { x: 715., y: -425. }];
|
|
let delayed = Event::SetBends {
|
|
context,
|
|
relation,
|
|
waypoints: waypoints.clone(),
|
|
};
|
|
send(&mut app, Message::Select(next));
|
|
assert_eq!(app.selected, next);
|
|
assert_eq!(
|
|
app.state_context(),
|
|
Some(context),
|
|
"same-view selection does not retire the context stamp"
|
|
);
|
|
let before = app.document.snapshot();
|
|
let camera = (
|
|
app.behavior.diagram.zoom,
|
|
app.behavior.diagram.pan,
|
|
app.behavior.diagram.fit,
|
|
);
|
|
event(&mut app, delayed);
|
|
assert_eq!(app.selected, next);
|
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
|
assert_eq!(app.project(), &before.decoded.project);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert!(!app.document.can_undo());
|
|
assert_eq!(
|
|
(
|
|
app.behavior.diagram.zoom,
|
|
app.behavior.diagram.pan,
|
|
app.behavior.diagram.fit
|
|
),
|
|
camera
|
|
);
|
|
send(&mut app, Message::Select(relation));
|
|
let context = app.state_context().unwrap();
|
|
event(
|
|
&mut app,
|
|
Event::SetBends {
|
|
context,
|
|
relation,
|
|
waypoints: waypoints.clone(),
|
|
},
|
|
);
|
|
let mut expected = before.decoded.project.clone();
|
|
let route = expected
|
|
.diagrams
|
|
.get_mut(&context.diagram)
|
|
.unwrap()
|
|
.routes
|
|
.iter_mut()
|
|
.find(|route| route.edge == relation)
|
|
.unwrap();
|
|
route.waypoints = waypoints;
|
|
assert_eq!(app.project(), &expected);
|
|
assert_source_model_agree(&app);
|
|
send(&mut app, Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert!(
|
|
!app.document.can_undo(),
|
|
"live drag remains a single transaction"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn state_bend_menu_carries_exact_segment_and_pin_without_changing_delete_action() {
|
|
let (mut app, _, relation, _) = setup();
|
|
let context = app.state_context().unwrap();
|
|
let point = Position { x: 500., y: -400. };
|
|
event(
|
|
&mut app,
|
|
Event::ContextMenu {
|
|
context,
|
|
object: relation,
|
|
placement: None,
|
|
anchor: iced::Point::ORIGIN,
|
|
bend: Some(1),
|
|
route_segment: Some((2, point)),
|
|
},
|
|
);
|
|
let menu = crate::model_menu::popup(&app).unwrap();
|
|
let mut add = false;
|
|
let mut remove = false;
|
|
let mut delete = false;
|
|
for entry in menu.entries {
|
|
if let crate::context_menu::Entry::Action {
|
|
message: Some(Message::Menu(crate::model_menu::Event::Invoke(_, message))),
|
|
..
|
|
} = entry
|
|
{
|
|
match *message {
|
|
Message::StateDiagram(Event::AddBend {
|
|
relation: id,
|
|
segment: Some((2, at)),
|
|
..
|
|
}) => {
|
|
assert_eq!(id, relation);
|
|
assert_eq!(at, point);
|
|
add = true;
|
|
}
|
|
Message::StateDiagram(Event::RemoveBend {
|
|
relation: id,
|
|
index: 1,
|
|
..
|
|
}) => {
|
|
assert_eq!(id, relation);
|
|
remove = true;
|
|
}
|
|
Message::Delete => delete = true,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
assert!(add && remove && delete);
|
|
}
|
|
|
|
fn setup() -> (App, Id, Id, Id) {
|
|
let mut f = fixture();
|
|
let diagram = create_view(&mut f).diagram;
|
|
let mut project = f.app.project().clone();
|
|
let relation = project.relate(RelationKind::StateTransition, "Bend trial", f.leaf, f.b);
|
|
let second = state_edit::create_view(&project, f.machine).unwrap();
|
|
project = prepare_command(&project, &second.command).unwrap().project;
|
|
project
|
|
.diagrams
|
|
.get_mut(&diagram)
|
|
.unwrap()
|
|
.routes
|
|
.push(EdgeRoute {
|
|
edge: relation,
|
|
waypoints: vec![
|
|
Position { x: 650., y: -400. },
|
|
Position { x: 300., y: -400. },
|
|
Position { x: 650., y: -400. },
|
|
],
|
|
label: Some(EdgeLabel {
|
|
fraction: 0.35,
|
|
offset: Position { x: 9., y: -23. },
|
|
}),
|
|
source_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Top,
|
|
offset: 0.234567,
|
|
}),
|
|
target_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Bottom,
|
|
offset: 0.75,
|
|
}),
|
|
});
|
|
let mut app = App::with_project(project);
|
|
send(&mut app, Message::RevealInDiagram(diagram, relation));
|
|
(app, diagram, relation, f.a)
|
|
}
|
|
|
|
#[test]
|
|
fn state_bend_commands_preserve_other_fields_views_and_exact_history() {
|
|
let (mut app, diagram, relation, _) = setup();
|
|
let baseline = app.document.snapshot();
|
|
let points = vec![Position { x: 715., y: 425. }];
|
|
let mut expected = app.project().clone();
|
|
expected.diagrams.get_mut(&diagram).unwrap().routes[0].waypoints = points.clone();
|
|
let context = app.state_context().unwrap();
|
|
event(
|
|
&mut app,
|
|
Event::SetBends {
|
|
context,
|
|
relation,
|
|
waypoints: points.clone(),
|
|
},
|
|
);
|
|
assert_eq!(app.project(), &expected);
|
|
assert_source_model_agree(&app);
|
|
let changed = app.document.snapshot();
|
|
let context = app.state_context().unwrap();
|
|
event(
|
|
&mut app,
|
|
Event::SetBends {
|
|
context,
|
|
relation,
|
|
waypoints: points,
|
|
},
|
|
);
|
|
assert!(
|
|
Arc::ptr_eq(&changed, &app.document.snapshot()),
|
|
"equal bends must not add history"
|
|
);
|
|
send(&mut app, Message::Undo);
|
|
assert_eq!(app.project(), &baseline.decoded.project);
|
|
assert_eq!(app.document.bundle(), &baseline.bundle);
|
|
assert!(!app.document.can_undo());
|
|
send(&mut app, Message::Redo);
|
|
assert_eq!(app.project(), &expected);
|
|
assert_eq!(app.document.bundle(), &changed.bundle);
|
|
let context = app.state_context().unwrap();
|
|
event(&mut app, Event::ClearBends { context, relation });
|
|
expected.diagrams.get_mut(&diagram).unwrap().routes[0]
|
|
.waypoints
|
|
.clear();
|
|
assert_eq!(app.project(), &expected);
|
|
let cleared = app.document.snapshot();
|
|
let context = app.state_context().unwrap();
|
|
event(&mut app, Event::ClearBends { context, relation });
|
|
assert!(Arc::ptr_eq(&cleared, &app.document.snapshot()));
|
|
}
|
|
|
|
#[test]
|
|
fn state_bend_self_loop_add_move_remove_clear_preserves_project_and_source_history() {
|
|
let (app, diagram, relation, _) = setup();
|
|
let mut project = app.project().clone();
|
|
let RelationBody::StateTransition { source, target, .. } =
|
|
&mut project.relations.get_mut(&relation).unwrap().body
|
|
else {
|
|
panic!("State transition fixture")
|
|
};
|
|
*target = *source;
|
|
project.diagrams.get_mut(&diagram).unwrap().routes[0]
|
|
.waypoints
|
|
.clear();
|
|
oa_core::validate_candidate(&project).unwrap();
|
|
let mut app = App::with_project(project);
|
|
send(&mut app, Message::RevealInDiagram(diagram, relation));
|
|
let baseline = app.document.snapshot();
|
|
let geometry = state_edit::bend_route(app.project(), diagram, relation).unwrap();
|
|
assert!(!geometry.custom_suspended);
|
|
assert!(geometry.pins.is_empty());
|
|
assert!(
|
|
geometry.points.len() >= 4,
|
|
"actual external self-loop routing"
|
|
);
|
|
let segment = 1;
|
|
let point = Position {
|
|
x: (geometry.points[segment].x + geometry.points[segment + 1].x) / 2.,
|
|
y: (geometry.points[segment].y + geometry.points[segment + 1].y) / 2.,
|
|
};
|
|
|
|
// Each operation must alter only the complete Project's waypoint list;
|
|
// Undo and Redo must restore exact serialized source, not just geometry.
|
|
fn check(app: &mut App, diagram: Id, mutation: Event, points: Vec<Position>) {
|
|
let before = app.document.snapshot();
|
|
let mut expected = before.decoded.project.clone();
|
|
expected.diagrams.get_mut(&diagram).unwrap().routes[0].waypoints = points;
|
|
event(app, mutation);
|
|
assert_eq!(app.project(), &expected);
|
|
assert_source_model_agree(app);
|
|
let after = app.document.snapshot();
|
|
assert_ne!(after.bundle, before.bundle);
|
|
send(app, Message::Undo);
|
|
assert_eq!(app.project(), &before.decoded.project);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
send(app, Message::Redo);
|
|
assert_eq!(app.project(), &expected);
|
|
assert_eq!(app.document.bundle(), &after.bundle);
|
|
assert_source_model_agree(app);
|
|
}
|
|
|
|
let context = app.state_context().unwrap();
|
|
check(
|
|
&mut app,
|
|
diagram,
|
|
Event::AddBend {
|
|
context,
|
|
relation,
|
|
segment: Some((segment, point)),
|
|
},
|
|
vec![point],
|
|
);
|
|
let moved = Position {
|
|
x: point.x + 45.,
|
|
y: point.y - 35.,
|
|
};
|
|
let context = app.state_context().unwrap();
|
|
check(
|
|
&mut app,
|
|
diagram,
|
|
Event::SetBends {
|
|
context,
|
|
relation,
|
|
waypoints: vec![moved],
|
|
},
|
|
vec![moved],
|
|
);
|
|
let context = app.state_context().unwrap();
|
|
check(
|
|
&mut app,
|
|
diagram,
|
|
Event::RemoveBend {
|
|
context,
|
|
relation,
|
|
index: 0,
|
|
},
|
|
vec![],
|
|
);
|
|
assert_eq!(app.project(), &baseline.decoded.project);
|
|
send(&mut app, Message::Undo);
|
|
let context = app.state_context().unwrap();
|
|
check(
|
|
&mut app,
|
|
diagram,
|
|
Event::ClearBends { context, relation },
|
|
vec![],
|
|
);
|
|
assert_eq!(app.project(), &baseline.decoded.project);
|
|
assert_source_model_agree(&app);
|
|
// Clear is a new source-preserving edit and may retain empty-list trivia.
|
|
// Exact original bytes are restored by history, not by reauthoring a value.
|
|
for _ in 0..3 {
|
|
assert!(app.document.can_undo());
|
|
send(&mut app, Message::Undo);
|
|
}
|
|
assert_eq!(app.project(), &baseline.decoded.project);
|
|
assert_eq!(app.document.bundle(), &baseline.bundle);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn state_bend_insertion_uses_captured_traversal_segment_and_removal_only_changes_pins() {
|
|
let (app, diagram, relation, _) = setup();
|
|
let context = app.state_context().unwrap();
|
|
let p = app.project();
|
|
let geometry = state_edit::bend_route(p, diagram, relation).unwrap();
|
|
let original = &p.diagrams[&diagram].routes[0];
|
|
let reverse = geometry
|
|
.points
|
|
.windows(2)
|
|
.position(|pair| pair[0] == original.waypoints[0] && pair[1] == original.waypoints[1])
|
|
.unwrap();
|
|
for (segment, expected_index) in [(reverse, 1), (reverse + 1, 2)] {
|
|
let point = Position { x: 500., y: -400. };
|
|
let planned = state_edit::plan(
|
|
p,
|
|
context,
|
|
&Event::AddBend {
|
|
context,
|
|
relation,
|
|
segment: Some((segment, point)),
|
|
},
|
|
)
|
|
.unwrap();
|
|
let actual = prepare_command(p, &planned.command).unwrap().project;
|
|
let mut expected = p.clone();
|
|
expected.diagrams.get_mut(&diagram).unwrap().routes[0]
|
|
.waypoints
|
|
.insert(expected_index, point);
|
|
assert_eq!(actual, expected);
|
|
let removed = state_edit::plan(
|
|
&actual,
|
|
context,
|
|
&Event::RemoveBend {
|
|
context,
|
|
relation,
|
|
index: expected_index,
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
prepare_command(&actual, &removed.command).unwrap().project,
|
|
*p
|
|
);
|
|
}
|
|
let default = state_edit::plan(
|
|
p,
|
|
context,
|
|
&Event::AddBend {
|
|
context,
|
|
relation,
|
|
segment: None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
let Command::SetEdgeRoute { route, .. } = default.command else {
|
|
panic!("route command")
|
|
};
|
|
assert_eq!(route.waypoints.len(), original.waypoints.len() + 1);
|
|
assert_eq!(route.source_attachment, original.source_attachment);
|
|
assert_eq!(route.target_attachment, original.target_attachment);
|
|
assert_eq!(route.label, original.label);
|
|
}
|
|
|
|
#[test]
|
|
fn state_bend_stale_nonfinite_capacity_hidden_and_folded_requests_are_inert() {
|
|
let (mut app, diagram, relation, composite) = setup();
|
|
let context = app.state_context().unwrap();
|
|
let baseline = app.document.snapshot();
|
|
for mutation in [
|
|
Event::SetBends {
|
|
context: Context {
|
|
epoch: context.epoch + 1,
|
|
..context
|
|
},
|
|
relation,
|
|
waypoints: vec![],
|
|
},
|
|
Event::SetBends {
|
|
context,
|
|
relation,
|
|
waypoints: vec![Position { x: f32::NAN, y: 0. }],
|
|
},
|
|
Event::SetBends {
|
|
context,
|
|
relation,
|
|
waypoints: vec![Position { x: 0., y: 0. }; 129],
|
|
},
|
|
Event::AddBend {
|
|
context,
|
|
relation,
|
|
segment: Some((usize::MAX, Position { x: 0., y: 0. })),
|
|
},
|
|
Event::RemoveBend {
|
|
context,
|
|
relation,
|
|
index: usize::MAX,
|
|
},
|
|
] {
|
|
event(&mut app, mutation);
|
|
assert!(Arc::ptr_eq(&baseline, &app.document.snapshot()));
|
|
}
|
|
let mut p = app.project().clone();
|
|
p.diagrams.get_mut(&diagram).unwrap().routes[0].waypoints =
|
|
vec![Position { x: 500., y: -400. }; 128];
|
|
assert!(
|
|
state_edit::plan(
|
|
&p,
|
|
context,
|
|
&Event::AddBend {
|
|
context,
|
|
relation,
|
|
segment: Some((1, Position { x: 500., y: -400. }))
|
|
}
|
|
)
|
|
.is_err()
|
|
);
|
|
let mut hidden = app.project().clone();
|
|
hidden
|
|
.diagrams
|
|
.get_mut(&diagram)
|
|
.unwrap()
|
|
.view_options
|
|
.hidden_edges
|
|
.push(relation);
|
|
assert!(state_edit::bend_route(&hidden, diagram, relation).is_none());
|
|
assert!(state_edit::plan(&hidden, context, &Event::ClearBends { context, relation }).is_err());
|
|
let owner = node(&app, diagram, composite);
|
|
let mut folded = app.project().clone();
|
|
folded
|
|
.diagrams
|
|
.get_mut(&diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&owner)
|
|
.unwrap()
|
|
.collapsed = true;
|
|
assert!(state_edit::bend_route(&folded, diagram, relation).is_none());
|
|
assert!(state_edit::plan(&folded, context, &Event::ClearBends { context, relation }).is_err());
|
|
let mut internal = app.project().clone();
|
|
internal.diagrams.get_mut(&diagram).unwrap().routes.clear();
|
|
if let RelationBody::StateTransition {
|
|
source,
|
|
target,
|
|
transition_kind,
|
|
..
|
|
} = &mut internal.relations.get_mut(&relation).unwrap().body
|
|
{
|
|
*target = *source;
|
|
*transition_kind = TransitionKind::Internal;
|
|
}
|
|
assert!(state_edit::bend_route(&internal, diagram, relation).is_none());
|
|
send(
|
|
&mut app,
|
|
Message::Field("/name".into(), "Unapplied bend owner name".into()),
|
|
);
|
|
let draft = app.draft.as_ref().unwrap().value.clone();
|
|
event(&mut app, Event::ClearBends { context, relation });
|
|
assert!(Arc::ptr_eq(&baseline, &app.document.snapshot()));
|
|
assert_eq!(app.draft.as_ref().unwrap().value, draft);
|
|
}
|