523 lines
19 KiB
Rust
523 lines
19 KiB
Rust
use super::*;
|
|
use crate::behavior::scenario_layout::Diagnostic;
|
|
use crate::document::Document;
|
|
use oa_core::{
|
|
Diagram, DiagramLayout, ProjectionKind, ScenarioLayout, language, source_project::ProjectLimits,
|
|
};
|
|
|
|
fn id(project: &Project, alias: &str) -> Id {
|
|
project
|
|
.identities()
|
|
.into_iter()
|
|
.find(|(_, name)| *name == alias)
|
|
.unwrap()
|
|
.0
|
|
}
|
|
|
|
fn fixture(layout: Option<ScenarioLayout>) -> (Document, Id, Id) {
|
|
let mut project = language::parse(FIXTURE).unwrap();
|
|
let owner = id(&project, "conversation");
|
|
let mut add = |alias: &str, layout: Option<ScenarioLayout>| {
|
|
let diagram = Id::new_v4();
|
|
project.diagrams.insert(
|
|
diagram,
|
|
Diagram {
|
|
id: diagram,
|
|
alias: alias.into(),
|
|
name: alias.into(),
|
|
description: String::new(),
|
|
layer: oa_core::Layer::Oa,
|
|
projection: ProjectionKind::Scenario { scenario: owner },
|
|
nodes: Default::default(),
|
|
routes: vec![],
|
|
view_options: Default::default(),
|
|
layout: layout.map(DiagramLayout::Scenario),
|
|
},
|
|
);
|
|
diagram
|
|
};
|
|
let a = add("saved_a", layout);
|
|
let b = add("saved_b", Some(ScenarioLayout::default()));
|
|
let document = Document::from_project(project, ProjectLimits::default()).unwrap();
|
|
(document, a, b)
|
|
}
|
|
|
|
fn resolved(project: &Project, diagram: Id, selected: Id) -> ResolvedScenarioPlan {
|
|
resolve(project, Representation::Saved { diagram }, selected).unwrap()
|
|
}
|
|
|
|
fn same_geometry(a: &ResolvedScenarioPlan, b: &ResolvedScenarioPlan) {
|
|
assert_eq!(a.bounds, b.bounds);
|
|
assert_eq!(a.lanes, b.lanes);
|
|
assert_eq!(a.plan.frames, b.plan.frames);
|
|
assert_eq!(a.plan.scopes, b.plan.scopes);
|
|
let (a, b) = (&a.scene, &b.scene);
|
|
assert_eq!(a.rows, b.rows);
|
|
assert_eq!(a.sequence_bounds, b.sequence_bounds);
|
|
assert_eq!(a.size, b.size);
|
|
assert_eq!(a.layout_origin, b.layout_origin);
|
|
assert_eq!(a.lanes, b.lanes);
|
|
assert_eq!(a.render_lanes, b.render_lanes);
|
|
assert_eq!(a.activations, b.activations);
|
|
assert_eq!(a.lifecycle, b.lifecycle);
|
|
assert_eq!(a.open_calls, b.open_calls);
|
|
let slots = |scene: &crate::behavior::Scene| {
|
|
scene
|
|
.slots
|
|
.iter()
|
|
.map(|item| {
|
|
(
|
|
item.slot.parent,
|
|
item.slot.before,
|
|
item.y,
|
|
item.label.clone(),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(slots(a), slots(b));
|
|
let lines = |scene: &crate::behavior::Scene| {
|
|
scene
|
|
.lines
|
|
.iter()
|
|
.map(|line| {
|
|
(
|
|
line.id,
|
|
line.points.clone(),
|
|
line.label_at,
|
|
line.label.clone(),
|
|
line.dashed,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let boxes = |scene: &crate::behavior::Scene| {
|
|
scene
|
|
.boxes
|
|
.iter()
|
|
.map(|item| {
|
|
(
|
|
item.id,
|
|
item.rect,
|
|
item.title.clone(),
|
|
item.detail.clone(),
|
|
item.marker,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(lines(a), lines(b));
|
|
assert_eq!(boxes(a), boxes(b));
|
|
}
|
|
|
|
fn expected_message(project: &Project, diagram: Id, occurrence: Id, y: f32) -> Project {
|
|
// Independent expected mutation: tests begin with no message overrides.
|
|
let mut expected = project.clone();
|
|
expected.diagrams.get_mut(&diagram).unwrap().layout =
|
|
Some(DiagramLayout::Scenario(ScenarioLayout {
|
|
lanes: vec![],
|
|
messages: vec![ScenarioMessagePosition {
|
|
address: OccurrenceEndpoint {
|
|
references: vec![],
|
|
occurrence,
|
|
},
|
|
send_y: y,
|
|
}],
|
|
}));
|
|
expected
|
|
}
|
|
|
|
#[test]
|
|
fn one_repeated_message_moves_in_one_view_with_exact_document_undo_redo_and_reopen() {
|
|
let (mut document, a, b) = fixture(None);
|
|
let original = document.snapshot();
|
|
let before = document.project().clone();
|
|
let first = id(&before, "first");
|
|
let second = id(&before, "second");
|
|
let baseline = resolved(&before, a, second);
|
|
assert_eq!(baseline.scene.rows[&first].1, 148.);
|
|
assert_eq!(baseline.scene.rows[&second].1, 240.);
|
|
let untouched = resolved(&before, b, second);
|
|
let drag = capture(&before, a, Target::Message(second)).unwrap();
|
|
let preview = drag.prepare(-40.).unwrap().unwrap();
|
|
let expected = expected_message(&before, a, second, 200.);
|
|
assert_eq!(preview.project, expected);
|
|
assert_eq!(preview.geometry.scene.rows[&first].1, 148.);
|
|
assert_eq!(preview.geometry.scene.rows[&second].1, 200.);
|
|
let line = preview
|
|
.geometry
|
|
.scene
|
|
.lines
|
|
.iter()
|
|
.find(|line| line.id == second)
|
|
.unwrap();
|
|
assert_eq!(line.points.first().unwrap().y, 200.);
|
|
assert_eq!(line.points.last().unwrap().y, 200.);
|
|
assert_eq!(
|
|
preview.geometry.scene.activations,
|
|
baseline.scene.activations
|
|
);
|
|
assert_eq!(preview.geometry.scene.lifecycle, baseline.scene.lifecycle);
|
|
same_geometry(&resolved(&preview.project, b, second), &untouched);
|
|
assert_eq!(drag.original, before);
|
|
assert!(std::sync::Arc::ptr_eq(&original, &document.snapshot()));
|
|
assert!(!document.can_undo());
|
|
|
|
let returned_selection = document.execute(preview.command).unwrap();
|
|
assert_eq!(
|
|
returned_selection,
|
|
Some(a),
|
|
"controller must preserve the dragged semantic selection separately"
|
|
);
|
|
assert_eq!(document.project(), &expected);
|
|
assert_eq!(document.revision(), 1);
|
|
let accepted = document.bundle().clone();
|
|
same_geometry(&resolved(document.project(), a, second), &preview.geometry);
|
|
assert!(document.undo());
|
|
assert_eq!(document.project(), &before);
|
|
assert_eq!(document.bundle(), &original.bundle);
|
|
assert!(!document.can_undo());
|
|
assert!(document.redo());
|
|
assert_eq!(document.project(), &expected);
|
|
assert_eq!(document.bundle(), &accepted);
|
|
let reopened = Document::from_bundle(accepted, ProjectLimits::default()).unwrap();
|
|
assert_eq!(reopened.project(), &expected);
|
|
same_geometry(&resolved(reopened.project(), a, second), &preview.geometry);
|
|
assert!(!reopened.dirty() && !reopened.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn horizontal_lane_move_changes_actual_header_and_attached_endpoints_only_in_selected_view() {
|
|
let (document, a, b) = fixture(None);
|
|
let before = document.project();
|
|
let right = id(before, "right");
|
|
let first = id(before, "first");
|
|
let other = resolved(before, b, right);
|
|
let preview = capture(before, a, Target::Lane(right))
|
|
.unwrap()
|
|
.prepare(100.)
|
|
.unwrap()
|
|
.unwrap();
|
|
let mut expected = before.clone();
|
|
expected.diagrams.get_mut(&a).unwrap().layout = Some(DiagramLayout::Scenario(ScenarioLayout {
|
|
lanes: vec![ScenarioLanePosition {
|
|
lifeline: right,
|
|
x: 500.,
|
|
}],
|
|
messages: vec![],
|
|
}));
|
|
assert_eq!(preview.project, expected);
|
|
assert_eq!(preview.geometry.scene.lanes[&right], 500.);
|
|
let header = preview
|
|
.geometry
|
|
.scene
|
|
.boxes
|
|
.iter()
|
|
.find(|item| item.id == right)
|
|
.unwrap();
|
|
assert_eq!(header.rect.center_x(), 500.);
|
|
let line = preview
|
|
.geometry
|
|
.scene
|
|
.lines
|
|
.iter()
|
|
.find(|line| line.id == first)
|
|
.unwrap();
|
|
assert_eq!(line.points.first().unwrap().x, 160.);
|
|
assert_eq!(line.points.last().unwrap().x, 500.);
|
|
assert!(line.points.iter().all(|point| point.y == 148.));
|
|
same_geometry(&resolved(&preview.project, b, right), &other);
|
|
assert_eq!(document.project(), before);
|
|
assert!(!document.dirty() && !document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn drawn_unassigned_lifeline_can_move_without_assigning_a_semantic_participant() {
|
|
let (document, a, _) = fixture(None);
|
|
let mut project = document.project().clone();
|
|
let owner = id(&project, "conversation");
|
|
let lane = project
|
|
.add_element(
|
|
Kind::Lifeline,
|
|
oa_core::Layer::Oa,
|
|
"Unassigned header",
|
|
Some(owner),
|
|
)
|
|
.unwrap();
|
|
let document = Document::from_project(project, ProjectLimits::default()).unwrap();
|
|
let baseline = resolved(document.project(), a, lane);
|
|
assert!(!baseline.scene.lanes.contains_key(&lane));
|
|
let before_header = baseline
|
|
.scene
|
|
.boxes
|
|
.iter()
|
|
.find(|item| item.id == lane)
|
|
.unwrap()
|
|
.rect;
|
|
let preview = capture(document.project(), a, Target::Lane(lane))
|
|
.unwrap()
|
|
.prepare(70.)
|
|
.unwrap()
|
|
.unwrap();
|
|
let after_header = preview
|
|
.geometry
|
|
.scene
|
|
.boxes
|
|
.iter()
|
|
.find(|item| item.id == lane)
|
|
.unwrap()
|
|
.rect;
|
|
assert_eq!(after_header.x, before_header.x + 70.);
|
|
assert_eq!(after_header.y, before_header.y);
|
|
assert_eq!(after_header.size(), before_header.size());
|
|
assert_eq!(preview.project.elements, document.project().elements);
|
|
assert_eq!(preview.project.relations, document.project().relations);
|
|
assert!(!document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn self_call_spacing_uses_send_y_and_preserves_reply_and_actual_activation_finish() {
|
|
let (document, a, _) = fixture(None);
|
|
let before = document.project();
|
|
let call = id(before, "call");
|
|
let reply = id(before, "reply");
|
|
let baseline = resolved(before, a, call);
|
|
let preview = capture(before, a, Target::Message(call))
|
|
.unwrap()
|
|
.prepare(-22.)
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(preview.project, expected_message(before, a, call, 310.));
|
|
let line = preview
|
|
.geometry
|
|
.scene
|
|
.lines
|
|
.iter()
|
|
.find(|line| line.id == call)
|
|
.unwrap();
|
|
assert_eq!(line.points.first().unwrap().y, 310.);
|
|
assert_eq!(line.points.last().unwrap().y, 334.);
|
|
assert_eq!(preview.geometry.scene.rows[&reply].1, 424.);
|
|
assert_eq!(preview.geometry.scene.activations.len(), 1);
|
|
let mut expected_activation = baseline.scene.activations.clone();
|
|
expected_activation[0].rect.y = 334.;
|
|
expected_activation[0].rect.height = 90.;
|
|
assert_eq!(preview.geometry.scene.activations, expected_activation);
|
|
assert_eq!(preview.geometry.scene.lifecycle, baseline.scene.lifecycle);
|
|
}
|
|
|
|
#[test]
|
|
fn feasible_spacing_crossing_remains_authored_with_diagnostic_and_unchanged_steps() {
|
|
let (document, a, _) = fixture(None);
|
|
let before = document.project();
|
|
let first = id(before, "first");
|
|
let second = id(before, "second");
|
|
let owner = id(before, "conversation");
|
|
let preview = capture(before, a, Target::Message(second))
|
|
.unwrap()
|
|
.prepare(-120.)
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(preview.project, expected_message(before, a, second, 120.));
|
|
assert_eq!(preview.geometry.scene.rows[&second].1, 120.);
|
|
assert_eq!(preview.geometry.scene.rows[&first].1, 148.);
|
|
assert_eq!(
|
|
preview.project.elements[&owner].steps(),
|
|
before.elements[&owner].steps()
|
|
);
|
|
let shown = crate::behavior::scenario_layout::presentation::present(
|
|
&preview.project,
|
|
Representation::Saved { diagram: a },
|
|
second,
|
|
);
|
|
assert!(shown.capabilities().coordinate_input);
|
|
assert!(
|
|
shown
|
|
.diagnostics()
|
|
.contains(&Diagnostic::SourceOrderCrossing {
|
|
scope: owner,
|
|
before: first,
|
|
after: second
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_self_call_move_is_refused_and_automatic_fallback_is_not_editable_capture() {
|
|
let (document, a, _) = fixture(None);
|
|
let before = document.project();
|
|
let call = id(before, "call");
|
|
let drag = capture(before, a, Target::Message(call)).unwrap();
|
|
let Err(Refusal::Geometry(issues)) = drag.prepare(118.) else {
|
|
panic!("send450 / receive474 after reply424 must be refused")
|
|
};
|
|
assert!(issues.iter().any(|issue| matches!(issue, GeometryIssue::TypedActivationCrossingNotSupported { call: id, .. } if *id == call)));
|
|
assert_eq!(&drag.original, before);
|
|
let invalid_geometry = expected_message(before, a, call, 450.);
|
|
let shown = crate::behavior::scenario_layout::presentation::present(
|
|
&invalid_geometry,
|
|
Representation::Saved { diagram: a },
|
|
call,
|
|
);
|
|
assert!(shown.capabilities().geometry_navigation && !shown.capabilities().coordinate_input);
|
|
assert!(matches!(
|
|
capture(&invalid_geometry, a, Target::Message(call)),
|
|
Err(Refusal::Geometry(_))
|
|
));
|
|
assert!(!document.dirty() && !document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_view_foreign_identity_and_latent_message_are_not_spacing_targets() {
|
|
let (document, a, b) = fixture(None);
|
|
let p = document.project();
|
|
let unfinished = id(p, "unfinished");
|
|
let source = id(p, "source");
|
|
assert!(matches!(
|
|
capture(p, Id::nil(), Target::Lane(source)),
|
|
Err(Refusal::InvalidSavedView(_))
|
|
));
|
|
for target in [
|
|
Target::Lane(source),
|
|
Target::Lane(b),
|
|
Target::Message(source),
|
|
Target::Message(unfinished),
|
|
] {
|
|
assert!(
|
|
matches!(capture(p, a, target), Err(Refusal::TargetNotRendered(found)) if found == target)
|
|
);
|
|
}
|
|
// A stored latent coordinate is not permission to fabricate a painted arrow.
|
|
let mut latent = expected_message(p, a, unfinished, 2000.);
|
|
let foreign_owner = latent
|
|
.add_element(Kind::Scenario, oa_core::Layer::Oa, "Foreign", None)
|
|
.unwrap();
|
|
let foreign_lane = latent
|
|
.add_element(
|
|
Kind::Lifeline,
|
|
oa_core::Layer::Oa,
|
|
"Foreign lane",
|
|
Some(foreign_owner),
|
|
)
|
|
.unwrap();
|
|
*latent
|
|
.elements
|
|
.get_mut(&foreign_lane)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Lifeline {
|
|
represented: Some(source),
|
|
};
|
|
let latent_document = Document::from_project(latent, ProjectLimits::default()).unwrap();
|
|
assert!(matches!(
|
|
capture(latent_document.project(), a, Target::Message(unfinished)),
|
|
Err(Refusal::TargetNotRendered(_))
|
|
));
|
|
assert!(matches!(
|
|
capture(latent_document.project(), a, Target::Lane(foreign_lane)),
|
|
Err(Refusal::TargetNotRendered(_))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn zero_motion_return_to_start_and_subprecision_preserve_sparse_intent_and_real_redo() {
|
|
let parsed = language::parse(FIXTURE).unwrap();
|
|
let left = id(&parsed, "left");
|
|
let second = id(&parsed, "second");
|
|
let layouts = [
|
|
None,
|
|
Some(ScenarioLayout::default()),
|
|
Some(ScenarioLayout {
|
|
lanes: vec![],
|
|
messages: vec![ScenarioMessagePosition {
|
|
address: OccurrenceEndpoint {
|
|
references: vec![],
|
|
occurrence: second,
|
|
},
|
|
send_y: 240.,
|
|
}],
|
|
}),
|
|
Some(ScenarioLayout {
|
|
lanes: vec![ScenarioLanePosition {
|
|
lifeline: left,
|
|
x: -0.,
|
|
}],
|
|
messages: vec![],
|
|
}),
|
|
];
|
|
for (index, layout) in layouts.into_iter().enumerate() {
|
|
let (mut document, a, _) = fixture(layout);
|
|
let owner = id(document.project(), "conversation");
|
|
document
|
|
.execute(Command::Rename(owner, "A real redo entry".into()))
|
|
.unwrap();
|
|
let redo = document.snapshot();
|
|
assert!(document.undo());
|
|
let before = document.snapshot();
|
|
let revision = document.revision();
|
|
let target = if index == 3 {
|
|
Target::Lane(left)
|
|
} else {
|
|
Target::Message(second)
|
|
};
|
|
let drag = capture(document.project(), a, target).unwrap();
|
|
if index == 3 {
|
|
let Some(DiagramLayout::Scenario(layout)) = &drag.original.diagrams[&a].layout else {
|
|
panic!()
|
|
};
|
|
assert_eq!(layout.lanes[0].x.to_bits(), (-0_f32).to_bits());
|
|
}
|
|
assert!(drag.prepare(10.).unwrap().is_some());
|
|
for delta in [0., -0.] {
|
|
assert!(drag.prepare(delta).unwrap().is_none());
|
|
}
|
|
if index != 3 {
|
|
assert!(drag.prepare(f32::from_bits(1)).unwrap().is_none());
|
|
}
|
|
assert_eq!(
|
|
serde_json::to_vec(&drag.original).unwrap(),
|
|
serde_json::to_vec(document.project()).unwrap()
|
|
);
|
|
assert!(std::sync::Arc::ptr_eq(&before, &document.snapshot()));
|
|
assert_eq!(document.bundle(), &before.bundle);
|
|
assert_eq!(document.revision(), revision);
|
|
assert!(document.can_redo());
|
|
assert!(document.redo());
|
|
assert_eq!(document.project(), &redo.decoded.project);
|
|
assert_eq!(document.bundle(), &redo.bundle);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_or_out_of_range_motion_cannot_mutate_original() {
|
|
let (document, a, _) = fixture(None);
|
|
let drag = capture(
|
|
document.project(),
|
|
a,
|
|
Target::Message(id(document.project(), "second")),
|
|
)
|
|
.unwrap();
|
|
for delta in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
|
|
assert!(matches!(drag.prepare(delta), Err(Refusal::InvalidDelta)));
|
|
}
|
|
assert!(matches!(drag.prepare(f32::MAX), Err(Refusal::Command(_))));
|
|
assert_eq!(&drag.original, document.project());
|
|
assert!(!document.dirty() && !document.can_undo());
|
|
}
|
|
|
|
const FIXTURE: &str = r#"arcadia 7;
|
|
project spacing_probe "Saved spacing probe" id "71000000-0000-4000-8000-000000000001" {
|
|
oa_activity source "Source" id "71000000-0000-4000-8000-000000000002" {}
|
|
oa_activity target "Target" id "71000000-0000-4000-8000-000000000003" {}
|
|
interaction exchange "Repeated exchange" id "71000000-0000-4000-8000-000000000004" { source @source; target @target; }
|
|
oa_scenario conversation "Conversation" id "71000000-0000-4000-8000-000000000005" { viewpoint OperationalActivity; steps [@first, @second, @call, @reply, @unfinished]; }
|
|
oa_lifeline left "Left" id "71000000-0000-4000-8000-000000000006" { parent @conversation; represented @source; }
|
|
oa_lifeline right "Right" id "71000000-0000-4000-8000-000000000007" { parent @conversation; represented @target; }
|
|
oa_occurrence first "First" id "71000000-0000-4000-8000-000000000008" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; }
|
|
oa_occurrence second "Second" id "71000000-0000-4000-8000-000000000009" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; }
|
|
oa_occurrence call "Call" id "71000000-0000-4000-8000-00000000000a" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Call; }; }; }
|
|
oa_occurrence reply "Reply" id "71000000-0000-4000-8000-00000000000b" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Reply; reply_to @call; }; }; }
|
|
oa_occurrence unfinished "Unfinished" id "71000000-0000-4000-8000-00000000000c" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver none; message_kind Asynchronous; }; }; }
|
|
}
|
|
"#;
|