Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/scenario_hit_tests.rs
T

387 lines
11 KiB
Rust

//! Actual Scene primitives, with independently chosen points and semantic IDs.
//! These preserve the existing label hit band; they do not establish ink bounds
//! or authoring eligibility, and do not dispatch a spacing gesture.
use super::*;
use crate::document::Document;
struct Fixture {
project: Project,
owner: Id,
sender: Id,
receiver: Id,
call: Id,
reply: Id,
exchange: Id,
}
fn fixture() -> Fixture {
let project = language::parse(r#"arcadia 7;
project hits "Hit roles" {
oa_activity source "Source" {}
oa_activity target "Target" {}
interaction exchange "Contract" { source @source; target @target; }
oa_scenario conversation "Conversation" { viewpoint OperationalActivity; steps [@call, @reply]; }
oa_lifeline left "Left" { parent @conversation; represented @source; }
oa_lifeline right "Right" { parent @conversation; represented @target; }
oa_occurrence call "Call" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Call; }; }; }
oa_occurrence reply "Reply" { parent @conversation; body { Exchange { exchange @exchange; sender @right; receiver @left; message_kind Reply; reply_to @call; }; }; }
}
"#).unwrap();
let id = |alias: &str| {
project
.identities()
.into_iter()
.find(|(_, name)| *name == alias)
.unwrap()
.0
};
Fixture {
owner: id("conversation"),
sender: id("left"),
receiver: id("right"),
call: id("call"),
reply: id("reply"),
exchange: id("exchange"),
project,
}
}
fn scene(f: &Fixture) -> Scene {
let document =
Document::from_project(f.project.clone(), source_project::ProjectLimits::default())
.unwrap();
let before = document.snapshot();
let result = Scene::sequence_geometry(
document.project(),
f.owner,
f.call,
Some(&BTreeMap::from([(f.sender, 160.), (f.receiver, 400.)])),
None,
);
assert_eq!(document.project(), &before.decoded.project);
assert_eq!(document.bundle(), &before.bundle);
assert_eq!(document.revision(), 0);
assert!(!document.can_undo() && !document.can_redo() && !document.dirty());
result
}
fn hit(scene: &Scene, point: Point, id: Id, role: SceneHitRole) {
assert_eq!(scene.hit_item(point), Some(SceneHit { id, role }));
assert_eq!(scene.hit(point), Some(id));
}
fn occurrence(p: &mut Project, parent: Id, body: OccurrenceBody) -> Id {
let mut element = p
.make_element(Kind::Occurrence, Layer::Oa, "Hit target", Some(parent))
.unwrap();
let id = element.id;
*element.shared_mut().unwrap() = SharedBody::Occurrence {
body,
precondition: String::new(),
postcondition: String::new(),
};
p.elements.insert(id, element);
p.elements
.get_mut(&parent)
.unwrap()
.steps_mut()
.unwrap()
.push(id);
id
}
fn exchange_mut(p: &mut Project, id: Id) -> &mut OccurrenceBody {
let SharedBody::Occurrence { body, .. } =
p.elements.get_mut(&id).unwrap().shared_mut().unwrap()
else {
panic!("expected occurrence")
};
body
}
#[test]
fn exact_headers_include_unassigned_lifelines_but_guides_are_not_headers() {
let mut f = fixture();
*f.project
.elements
.get_mut(&f.receiver)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Lifeline { represented: None };
let s = scene(&f);
assert!(!s.lanes.contains_key(&f.receiver));
assert_eq!(s.render_lanes[&f.receiver], 400.);
for (id, x) in [(f.sender, 160.), (f.receiver, 400.)] {
// The actual header is x-102..x+102, y15..85, including its padding.
for point in [Point::new(x - 101., 16.), Point::new(x + 101., 84.)] {
hit(&s, point, id, SceneHitRole::LifelineHeader);
}
hit(&s, Point::new(x, 95.), id, SceneHitRole::LifelineGuide);
}
}
#[test]
fn repeated_exchange_occurrences_keep_distinct_message_body_and_label_hits() {
let f = fixture();
let s = scene(&f);
assert_eq!(s.rows[&f.call].1, 148.);
assert_eq!(s.rows[&f.reply].1, 240.);
for (id, y) in [(f.call, 148.), (f.reply, 240.)] {
hit(&s, Point::new(280., y), id, SceneHitRole::MessageBody);
hit(
&s,
Point::new(180., y - 16.),
id,
SceneHitRole::MessageLabel,
);
assert_ne!(id, f.exchange);
}
}
#[test]
fn self_call_elbow_and_receive_endpoint_are_messages_not_execution_hits() {
let mut f = fixture();
for id in [f.call, f.reply] {
let OccurrenceBody::Exchange {
sender,
receiver,
exchange,
..
} = exchange_mut(&mut f.project, id)
else {
unreachable!()
};
*sender = Some(f.sender);
*receiver = Some(f.sender);
*exchange = None;
}
let s = scene(&f);
let call = s.lines.iter().find(|l| l.id == f.call).unwrap();
assert_eq!(call.points.len(), 4);
assert_eq!(call.points[0].y, 148.);
assert_eq!(call.points[3].y, 172.);
hit(
&s,
Point::new(call.points[1].x, 160.),
f.call,
SceneHitRole::MessageBody,
);
hit(&s, call.points[3], f.call, SceneHitRole::MessageBody);
hit(
&s,
Point::new(180., 132.),
f.call,
SceneHitRole::MessageLabel,
);
let bar = s.activations.iter().find(|a| a.call == f.call).unwrap();
hit(
&s,
Point::new(bar.rect.x + 2., 190.),
f.call,
SceneHitRole::Activation,
);
}
#[test]
fn activation_shoulder_beats_another_calls_label_and_broad_message_tolerance() {
let mut f = fixture();
let mut inner = f.project.elements[&f.call].clone();
inner.id = Id::new_v4();
inner.alias = "inner_call_hit".into();
inner.name = "Pressure verification with a descriptive operation label".into();
let inner_id = inner.id;
f.project.elements.insert(inner_id, inner);
let mut inner_reply = f.project.elements[&f.reply].clone();
inner_reply.id = Id::new_v4();
inner_reply.alias = "inner_reply_hit".into();
let reply_id = inner_reply.id;
f.project.elements.insert(reply_id, inner_reply);
let OccurrenceBody::Exchange { reply_to, .. } = exchange_mut(&mut f.project, reply_id) else {
unreachable!()
};
*reply_to = Some(inner_id);
*f.project
.elements
.get_mut(&f.owner)
.unwrap()
.steps_mut()
.unwrap() = vec![f.call, inner_id, reply_id, f.reply];
let mut s = scene(&f);
let inner = s.lines.iter().find(|l| l.id == inner_id).unwrap();
let outer = s.activations.iter().find(|a| a.call == f.call).unwrap();
let shoulder = Point::new(outer.rect.x + 1., 225.);
assert!(outer.rect.contains(shoulder));
assert!(inner.label_bounds().contains(shoulder));
let endpoint = *inner.points.last().unwrap();
for scale in [1., 0.05, 0.0003] {
s.scale = scale;
hit(&s, shoulder, f.call, SceneHitRole::Activation);
hit(&s, endpoint, inner_id, SceneHitRole::MessageBody);
hit(
&s,
Point::new(180., 225.),
inner_id,
SceneHitRole::MessageLabel,
);
}
}
#[test]
fn create_delete_markers_are_distinct_from_their_own_message_body_and_label() {
let mut f = fixture();
for (id, kind) in [
(f.call, MessageKind::Create),
(f.reply, MessageKind::Delete),
] {
let OccurrenceBody::Exchange {
sender,
receiver,
message_kind,
reply_to,
..
} = exchange_mut(&mut f.project, id)
else {
unreachable!()
};
*sender = Some(f.sender);
*receiver = Some(f.receiver);
*message_kind = kind;
*reply_to = None;
}
let s = scene(&f);
for (id, y, label_y) in [(f.call, 148., 118.), (f.reply, 240., 224.)] {
hit(&s, Point::new(400., y), id, SceneHitRole::LifecycleMarker);
hit(&s, Point::new(280., y), id, SceneHitRole::MessageBody);
hit(
&s,
Point::new(180., label_y),
id,
SceneHitRole::MessageLabel,
);
}
// Capsule boundary owns its contact with the incoming Create arrow.
hit(
&s,
Point::new(330., 148.),
f.call,
SceneHitRole::LifecycleMarker,
);
}
#[test]
fn tiny_scale_nearest_hit_retains_the_winning_marker_or_message_role() {
let mut f = fixture();
let OccurrenceBody::Exchange {
sender,
receiver,
message_kind,
reply_to,
..
} = exchange_mut(&mut f.project, f.reply)
else {
unreachable!()
};
*sender = Some(f.sender);
*receiver = Some(f.receiver);
*message_kind = MessageKind::Delete;
*reply_to = None;
let mut s = scene(&f);
s.scale = 0.0003;
// Both are outside exact strokes/labels. Comparing all broad candidates
// chooses the actual nearer shape, even when their UUID is identical.
hit(
&s,
Point::new(280., 246.),
f.reply,
SceneHitRole::MessageBody,
);
hit(
&s,
Point::new(400., 265.),
f.reply,
SceneHitRole::LifecycleMarker,
);
hit(
&s,
Point::new(280., 154.),
f.call,
SceneHitRole::MessageBody,
);
}
#[test]
fn action_frame_and_use_boxes_do_not_advertise_message_or_header_roles() {
for body in [
OccurrenceBody::Action {
action: None,
lifeline: None,
},
OccurrenceBody::Fragment {
operator: FragmentOperator::Alternative,
branches: vec![],
},
OccurrenceBody::ScenarioUse {
scenario: None,
bindings: vec![],
},
OccurrenceBody::Exchange {
exchange: None,
sender: None,
receiver: None,
message_kind: MessageKind::Asynchronous,
operation: None,
reply_to: None,
values: vec![],
},
] {
let mut f = fixture();
let id = occurrence(&mut f.project, f.owner, body);
let s = scene(&f);
let item = s.boxes.iter().find(|b| b.id == id).unwrap();
hit(
&s,
item.rect.position() + iced::Vector::new(15., 15.),
id,
SceneHitRole::Box,
);
}
}
#[test]
fn winning_primitive_role_does_not_come_from_its_shared_uuid() {
let f = fixture();
let mut s = scene(&f);
// A controlled overlapping primitive verifies reverse box order without
// inventing a core alias. Its ID intentionally equals a real header's ID.
s.boxes.push(BoxItem {
id: f.sender,
role: SceneHitRole::Box,
rect: Rectangle::new(Point::new(150., 20.), Size::new(20., 20.)),
title: "Overlay control".into(),
detail: String::new(),
marker: None,
});
hit(&s, Point::new(160., 30.), f.sender, SceneHitRole::Box);
hit(
&s,
Point::new(180., 30.),
f.sender,
SceneHitRole::LifelineHeader,
);
}
#[test]
fn invalid_points_never_produce_a_semantic_or_spacing_hit() {
let f = fixture();
let s = scene(&f);
for point in [
Point::new(f32::NAN, 148.),
Point::new(280., f32::NAN),
Point::new(f32::INFINITY, 148.),
Point::new(280., f32::NEG_INFINITY),
] {
assert_eq!(s.hit_item(point), None);
assert_eq!(s.hit(point), None);
}
}