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

1663 lines
57 KiB
Rust

use crate::{
app::{App, LoadedSnapshot, Message, Page, Pending},
forms::Draft,
workflow::{Event, Workflow},
};
use iced::widget::text_editor;
use oa_core::*;
use serde_json::json;
fn send(app: &mut App, m: Message) {
let _ = app.update(m);
}
fn fixture() -> Project {
let mut p = Project::empty();
let owner = p.add(Kind::Entity, "Rescue team", 40., 40.);
let work = p.add(Kind::Activity, "Assess incident", 340., 40.);
p.relate(RelationKind::Allocation, "Performs", owner, work);
p
}
#[test]
fn focused_explorer_follows_reparenting_without_changing_identity() {
use crate::explorer::Group;
let mut p = Project::empty();
let system = p
.add_element(Kind::System, Layer::Sa, "Platform", None)
.unwrap();
let machine = p
.add_element(Kind::StateMachine, Layer::Sa, "Operating behavior", None)
.unwrap();
let mut app = App::with_project(p);
send(&mut app, Message::Explore(Some(machine)));
assert_eq!(app.explorer_group, Some(Group::Behavior));
send(&mut app, Message::ExplorerCollapseAll);
send(
&mut app,
Message::Command(Command::Reparent {
element: machine,
parent: Some(system),
}),
);
assert_eq!(
app.project().elements[&machine].parent,
Some(system),
"{}",
app.status
);
assert_eq!(app.selected, machine);
assert_eq!(app.explorer_focus, Some(machine));
assert_eq!(app.explorer_group, Some(Group::Participants));
assert!(!app.explorer_collapsed.contains(&system));
assert!(!app.explorer_groups_collapsed.contains(&Group::Participants));
send(&mut app, Message::Undo);
assert_eq!(app.explorer_group, Some(Group::Behavior));
send(&mut app, Message::Redo);
assert_eq!(app.explorer_group, Some(Group::Participants));
}
#[test]
fn actor_port_creation_updates_containment_and_locate_reveals_its_owner() {
let mut project = Project::empty();
let actor = project
.add_element(Kind::SystemActor, Layer::Sa, "External operator", None)
.unwrap();
let mut app = App::with_project(project.clone());
send(&mut app, Message::Explore(Some(actor)));
send(
&mut app,
Message::PortAction(crate::port_workflow::Action::Component),
);
assert!(app.workflow.is_some(), "{}", app.status);
send(&mut app, Message::Workflow(Event::Apply));
let port = app.selected;
assert_eq!(app.project().elements[&port].parent, Some(actor));
assert!(matches!(
port_contract(&app.project().elements[&port]),
Some(PortContract::Component {
kind: ComponentPortKind::Standard,
..
})
));
assert!(app.explorer_index.children(actor).contains(&port));
send(&mut app, Message::ExplorerCollapseAll);
send(&mut app, Message::Search("unrelated filter".into()));
send(&mut app, Message::ExplorerLocate);
assert!(app.search.is_empty());
assert!(!app.explorer_collapsed.contains(&actor));
assert!(
!app.explorer_groups_collapsed
.contains(&crate::explorer::Group::Participants)
);
send(&mut app, Message::AddRelation);
let Some(Workflow::Relation(relation)) = &app.workflow else {
panic!("boundary workflow")
};
assert_eq!(relation.kind, RelationKind::BoundaryExchange);
assert_eq!(relation.source.as_ref().map(|c| c.id), Some(port));
send(&mut app, Message::Workflow(Event::Cancel));
send(&mut app, Message::Undo);
assert_eq!(app.project(), &project);
assert!(app.explorer_index.children(actor).is_empty());
send(&mut app, Message::Redo);
assert!(app.explorer_index.children(actor).contains(&port));
}
#[test]
fn pending_source_prevents_property_draft_collision_and_preserves_both_if_present() {
let mut app = App::with_project(Project::empty());
let original = app.project().clone();
send(&mut app, Message::Page(Page::Text));
let source = format!("// Review rationale\n{}", app.source_editor.text());
send(
&mut app,
Message::SourceEdit(text_editor::Action::SelectAll),
);
send(
&mut app,
Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste(
std::sync::Arc::new(source.clone()),
))),
);
send(&mut app, Message::Page(Page::Diagram));
send(
&mut app,
Message::Field(
"/name".into(),
"Must not enter a hidden property draft".into(),
),
);
assert!(!app.draft_dirty());
assert_eq!(app.source_editor.text(), source);
assert_eq!(app.project(), &original);
// A stale adapter or future integration must not let Apply drop an already
// present property draft either. Preserve both and require an explicit choice.
app.draft
.as_mut()
.unwrap()
.input("/name", "Retained property draft".into());
assert!(app.draft_dirty());
assert!(!app.apply_source());
assert!(app.source_dirty());
assert!(app.draft_dirty());
assert_eq!(app.source_editor.text(), source);
assert_eq!(app.project(), &original);
send(&mut app, Message::RevertProperties);
assert!(app.apply_source());
send(
&mut app,
Message::Field("/name".into(), "Reviewed model".into()),
);
send(&mut app, Message::Apply);
assert_eq!(app.project().name, "Reviewed model");
assert!(
app.document.bundle().files[&app.document.bundle().entry]
.starts_with("// Review rationale")
);
}
#[test]
fn system_only_models_open_the_populated_perspective() {
let mut p = Project::empty();
p.add_element(Kind::Function, Layer::Sa, "Receive report", None)
.unwrap();
let diagram = p.ensure_diagram(Layer::Sa);
let app = App::with_project(p);
assert_eq!(app.layer, Layer::Sa);
assert_eq!(app.active_diagram, Some(diagram));
assert_eq!(app.create_kind, Kind::Function);
assert_eq!(App::with_project(Project::empty()).layer, Layer::Oa);
assert_eq!(App::with_project(Project::demo()).layer, Layer::Oa);
}
fn source(app: &mut App, text: &str) {
send(app, Message::Page(Page::Text));
send(app, Message::SourceEdit(text_editor::Action::SelectAll));
send(
app,
Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste(
std::sync::Arc::new(text.into()),
))),
);
}
#[test]
fn opening_and_creating_models_preserve_the_actual_window_size_for_fit() {
let size = iced::Size::new(1100., 700.);
let project = fixture();
let mut expected = App::with_project(project.clone());
send(&mut expected, Message::WindowResized(size));
send(&mut expected, Message::Fit);
let mut app = App::with_project(Project::empty());
send(&mut app, Message::WindowResized(size));
send(
&mut app,
Message::Loaded(Ok(Some(LoadedSnapshot::Legacy(
std::path::PathBuf::from("resized-model.arcadia"),
Box::new(persistence::DecodeResult {
project: project.clone(),
migration: None,
}),
std::sync::Arc::from(language::format(&project).into_bytes()),
)))),
);
assert_eq!(app.window_size, size);
assert_eq!(app.zoom, expected.zoom);
assert_eq!(app.pan, expected.pan);
send(&mut app, Message::Fit);
assert_eq!(app.zoom, expected.zoom);
send(&mut app, Message::Request(Pending::New));
assert!(app.project().elements.is_empty());
assert_eq!(app.window_size, size);
send(&mut app, Message::Request(Pending::Demo));
assert_eq!(app.window_size, size);
let mut expected_demo = App::with_project(app.project().clone());
send(&mut expected_demo, Message::WindowResized(size));
send(&mut expected_demo, Message::Fit);
assert_eq!(app.zoom, expected_demo.zoom);
assert_eq!(app.pan, expected_demo.pan);
}
#[test]
fn every_demo_record_has_a_lossless_graphical_draft() {
let p = Project::demo();
for (id, _) in p.identities() {
let draft = Draft::from_project(&p, id).expect("Graphical record");
assert!(!draft.dirty());
let candidate = execute_batch(&p, &[draft.command().unwrap()]).unwrap();
assert_eq!(candidate, p, "untouched property form changed {id}");
}
}
#[test]
fn opening_legacy_source_surfaces_report_without_modifying_original_bytes() {
let bytes: std::sync::Arc<[u8]> = std::sync::Arc::from(
include_bytes!("../../oa-core/tests/fixtures/v2-coastal-rescue.json").as_slice(),
);
let decoded = persistence::decode_with_report(&bytes).unwrap();
let original = decoded.project.clone();
let report = decoded.migration.clone().unwrap();
let mut app = App::with_project(Project::empty());
send(
&mut app,
Message::Loaded(Ok(Some(LoadedSnapshot::Legacy(
std::path::PathBuf::from("legacy.json"),
Box::new(decoded),
bytes.clone(),
)))),
);
assert_eq!(app.migration, Some(report));
assert_eq!(app.page, Page::Validation);
assert_eq!(app.project(), &original);
assert!(!app.dirty());
assert!(app.source_snapshot.starts_with("arcadia 3;"));
assert!(serde_json::from_slice::<serde_json::Value>(&bytes).unwrap()["schema_version"] == 2);
}
#[test]
fn fit_uses_measured_canvas_bounds_and_preserves_them_on_open() {
let p = fixture();
let mut app = App::with_project(p.clone());
let canvas = iced::Size::new(510., 275.);
send(&mut app, Message::CanvasResized(canvas));
send(
&mut app,
Message::Loaded(Ok(Some(LoadedSnapshot::Legacy(
std::path::PathBuf::from("measured.arcadia"),
Box::new(persistence::DecodeResult {
project: p.clone(),
migration: None,
}),
std::sync::Arc::from(language::format(&p).into_bytes()),
)))),
);
send(&mut app, Message::Fit);
for node in projection::project_view(app.project(), app.active_diagram.unwrap()).nodes {
let x = app.pan.x + node.position.x * app.zoom;
let y = app.pan.y + node.position.y * app.zoom;
assert!(x >= 23.9 && y >= 23.9);
assert!(x + node.size.width * app.zoom <= canvas.width - 23.9);
assert!(y + node.size.height * app.zoom <= canvas.height - 23.9);
}
}
#[test]
fn fit_adapts_to_canvas_returning_after_help_but_preserves_manual_navigation() {
let p = fixture();
let mut app = App::with_project(p);
send(
&mut app,
Message::CanvasResized(iced::Size::new(900., 600.)),
);
send(&mut app, Message::Page(Page::Guide));
send(
&mut app,
Message::WindowResized(iced::Size::new(1100., 700.)),
);
send(&mut app, Message::Page(Page::Diagram));
let compact = iced::Size::new(500., 270.);
send(&mut app, Message::CanvasResized(compact));
let fitted = (app.zoom, app.pan);
send(&mut app, Message::Fit);
assert_eq!((app.zoom, app.pan), fitted);
send(&mut app, Message::Zoom(1.2));
let manual = (app.zoom, app.pan);
send(
&mut app,
Message::CanvasResized(iced::Size::new(650., 400.)),
);
assert_eq!((app.zoom, app.pan), manual);
}
#[test]
fn reopening_canvas_for_a_modal_preserves_viewport_after_route_edits() {
let mut p = Project::empty();
let sender = p.add(Kind::Activity, "Report", 0., 0.);
let target = p.add(Kind::Activity, "Receive", 600., 0.);
let edge = p.relate(RelationKind::Interaction, "Report signal", sender, target);
let diagram = p.ensure_diagram(Layer::Oa);
let mut app = App::with_project(p);
let canvas = iced::Size::new(900., 600.);
send(&mut app, Message::CanvasResized(canvas));
let viewport = (app.zoom, app.pan);
send(
&mut app,
Message::Command(Command::SetEdgeRoute {
diagram,
route: EdgeRoute {
edge,
waypoints: vec![Position { x: 400., y: 400. }],
label: Some(EdgeLabel {
fraction: 0.5,
offset: Position { x: 0., y: 150. },
}),
},
}),
);
assert_eq!((app.zoom, app.pan), viewport);
let mut fitted = App::with_project(app.project().clone());
send(&mut fitted, Message::CanvasResized(canvas));
assert_ne!(
(fitted.zoom, fitted.pan),
viewport,
"The edited route must change the fitted bounds to expose an accidental refit"
);
// The shared file modal remounts the canvas sensor. Its on_show measurement
// describes the existing viewport, not a user request to fit the edited model.
send(&mut app, Message::Export);
assert!(app.file_prompt.is_some());
send(&mut app, Message::CanvasResized(canvas));
assert_eq!((app.zoom, app.pan), viewport);
send(&mut app, Message::CancelFile);
send(&mut app, Message::CanvasResized(canvas));
assert_eq!((app.zoom, app.pan), viewport);
let compact = iced::Size::new(650., 400.);
send(&mut app, Message::CanvasResized(compact));
send(&mut fitted, Message::CanvasResized(compact));
assert_eq!((app.zoom, app.pan), (fitted.zoom, fitted.pan));
assert_ne!((app.zoom, app.pan), viewport);
}
#[test]
fn source_navigation_waits_for_editor_layout_and_cancels_on_edit() {
let p = fixture();
let id = p
.elements
.values()
.find(|e| e.kind() == Kind::Activity)
.unwrap()
.id;
let mut app = App::with_project(p);
send(&mut app, Message::Select(id));
send(&mut app, Message::SourceForSelection);
assert_eq!(app.page, Page::Text);
assert!(app.source_jump.is_some());
send(&mut app, Message::SourceViewport);
assert!(app.source_jump.is_none());
send(&mut app, Message::SourceForSelection);
assert!(app.source_jump.is_some());
send(
&mut app,
Message::SourceEdit(text_editor::Action::SelectAll),
);
assert!(app.source_jump.is_none());
}
#[test]
fn source_find_navigates_an_invalid_draft_without_applying_or_losing_it() {
use crate::source_navigation::Event;
let mut app = App::with_project(fixture());
let accepted = app.project().clone();
source(&mut app, "é 救助\nthis is an unfinished model: 救助");
send(&mut app, Message::SourceNavigation(Event::Open(false)));
send(
&mut app,
Message::SourceNavigation(Event::Query("救助".into())),
);
send(&mut app, Message::SourceNavigation(Event::Next(false)));
let cursor = app.source_jump.unwrap();
assert_eq!(
cursor.position,
text_editor::Position { line: 0, column: 3 }
);
assert_eq!(
cursor.selection,
Some(text_editor::Position { line: 0, column: 9 })
);
send(&mut app, Message::SourceViewport);
assert_eq!(app.source_editor.cursor(), cursor);
send(
&mut app,
Message::SourceNavigation(Event::Line("99".into())),
);
send(&mut app, Message::SourceNavigation(Event::Go));
assert!(app.status.contains("1 to 2"));
assert_eq!(*app.project(), accepted);
assert!(app.source_dirty());
}
#[test]
fn graphical_rename_keeps_aliases_and_references_and_undo() {
let mut app = App::with_project(fixture());
let before = app.project().clone();
let id = before
.elements
.values()
.find(|e| e.kind() == Kind::Activity)
.unwrap()
.id;
send(&mut app, Message::Select(id));
send(
&mut app,
Message::Field("/name".into(), "Coordinate rescue".into()),
);
send(&mut app, Message::Apply);
assert_eq!(
app.project().elements[&id].alias,
before.elements[&id].alias
);
assert_eq!(
crate::source_workspace::compile(&app.source_workspace.bundle())
.unwrap()
.decoded
.project,
*app.project()
);
send(&mut app, Message::Undo);
assert_eq!(*app.project(), before);
}
#[test]
fn identity_property_change_is_atomic_and_rewrites_all_live_references() {
let mut app = App::with_project(fixture());
let before = app.project().clone();
let old = before
.elements
.values()
.find(|e| e.kind() == Kind::Activity)
.unwrap()
.id;
let new = Id::new_v4();
send(&mut app, Message::Select(old));
send(&mut app, Message::Field("/id".into(), new.to_string()));
send(
&mut app,
Message::Field("/name".into(), "New identity".into()),
);
send(&mut app, Message::Apply);
assert!(!app.project().contains(old));
assert!(app.project().relations.values().any(|r| r.target() == new));
assert!(
app.project()
.diagrams
.values()
.any(|d| d.nodes.values().any(|n| n.element == new))
);
send(&mut app, Message::Undo);
assert_eq!(*app.project(), before);
}
#[test]
fn invalid_numeric_draft_survives_navigation_save_and_help() {
let mut app = App::with_project(fixture());
let p = app.project().clone();
let node = p
.diagrams
.values()
.next()
.unwrap()
.nodes
.values()
.next()
.unwrap()
.id;
send(&mut app, Message::Select(node));
send(&mut app, Message::Field("/position/x".into(), "-".into()));
send(&mut app, Message::Save(false));
assert!(!app.busy);
assert_eq!(*app.project(), p);
send(&mut app, Message::Page(Page::Guide));
send(&mut app, Message::Page(Page::Diagram));
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.draft.as_ref().unwrap().inputs["/position/x"].raw, "-");
send(&mut app, Message::SelectProject);
assert_eq!(app.selected, node);
}
#[test]
fn malformed_source_preserves_model_and_is_recoverable() {
let mut app = App::with_project(fixture());
let p = app.project().clone();
source(&mut app, "arcadia 2; ?");
send(&mut app, Message::ApplySource);
assert_eq!(*app.project(), p);
assert!(!app.source_diagnostics.is_empty());
send(&mut app, Message::Request(Pending::New));
assert!(app.pending.is_some());
send(&mut app, Message::Cancel);
assert_eq!(app.source_editor.text(), "arcadia 2; ?");
send(&mut app, Message::RevertSource);
assert_eq!(
crate::source_workspace::compile(&app.source_workspace.bundle())
.unwrap()
.decoded
.project,
p
);
}
#[test]
fn escaped_string_mode_preserves_exact_control_characters() {
let mut app = App::with_project(fixture());
send(&mut app, Message::ExactStrings(true));
let exact = "Plan\r\nRescue\t\"North\"";
send(
&mut app,
Message::Field("/name".into(), serde_json::to_string(exact).unwrap()),
);
send(&mut app, Message::Apply);
assert_eq!(app.project().name, exact);
assert!(!app.draft_dirty());
send(
&mut app,
Message::Field("/name".into(), "not quoted".into()),
);
send(&mut app, Message::Apply);
assert_eq!(app.project().name, exact);
assert!(app.draft_dirty());
send(&mut app, Message::RevertProperties);
assert!(!app.draft_dirty());
}
#[test]
fn explicit_relationship_builder_never_mutates_on_preview() {
let mut p = Project::empty();
let owner = p.add(Kind::Entity, "Team", 0., 0.);
let work = p.add(Kind::Activity, "Respond", 300., 0.);
let mut workflow = Workflow::relationship(&p, owner, Layer::Oa);
let before = p.clone();
workflow
.update(&p, Event::RelationName("Performs".into()))
.unwrap();
workflow
.update(
&p,
Event::Target(crate::app::Choice {
id: work,
label: "Respond".into(),
}),
)
.unwrap();
assert_eq!(p, before);
let result = workflow.update(&p, Event::Apply).unwrap().unwrap();
assert_eq!(result.relations.len(), 1);
}
#[test]
fn explorer_and_creation_selection_follow_their_own_placement() {
let p = fixture();
let owner = p
.elements
.values()
.find(|e| e.kind() == Kind::Entity)
.unwrap()
.id;
let work = p
.elements
.values()
.find(|e| e.kind() == Kind::Activity)
.unwrap()
.id;
let mut app = App::with_project(p);
send(&mut app, Message::Select(owner));
let owner_node = app.selected_node.expect("placed explorer selection");
send(&mut app, Message::Select(work));
let work_node = app.selected_node.expect("placed work selection");
assert_ne!(owner_node, work_node);
let before = app.project().clone();
send(&mut app, Message::PlaceSelected);
assert_eq!(
*app.project(),
before,
"showing an existing placement does not create a duplicate"
);
send(&mut app, Message::CreateKind(Kind::Capability));
send(&mut app, Message::Add(false));
let diagram = &app.project().diagrams[&app.active_diagram.unwrap()];
let new_node = &diagram.nodes[&app.selected_node.expect("new element placement")];
assert_eq!(new_node.element, app.selected);
assert_ne!(
new_node.id, work_node,
"placement actions cannot target previously selected work"
);
}
#[test]
fn bidirectional_transition_ui_commits_once_and_preserves_other_diagrams() {
let mut p = Project::empty();
let activity = p.add(Kind::Activity, "Coordinate rescue", 120.5, 85.25);
let original = p.clone();
let mut w = Workflow::transition(&p, activity).unwrap();
let next = w.update(&p, Event::Apply).unwrap().unwrap();
assert_eq!(
next.diagrams[&original.diagrams.keys().copied().next().unwrap()],
*original.diagrams.values().next().unwrap()
);
let function = next
.elements
.values()
.find(|e| e.kind() == Kind::Function)
.unwrap()
.id;
let mut reverse = Workflow::transition(&next, function).unwrap();
let again = reverse.update(&next, Event::Apply).unwrap().unwrap();
assert_eq!(again.elements.len(), next.elements.len());
assert_eq!(again.traces.len(), 1);
let mut app = App::with_project(original.clone());
send(&mut app, Message::Command(Command::Replace(next)));
send(&mut app, Message::Undo);
assert_eq!(*app.project(), original);
}
#[test]
fn mission_capability_authoring_is_contextual_atomic_and_text_equivalent() {
for (layer, mission_kind, capability_kind, participant_kind) in [
(
Layer::Oa,
Kind::OperationalMission,
Kind::Capability,
Kind::Actor,
),
(
Layer::Sa,
Kind::Mission,
Kind::SystemCapability,
Kind::SystemActor,
),
] {
let mut p = Project::empty();
let mission = p
.add_element(mission_kind, layer, "Protect life", None)
.unwrap();
let base = p
.add_element(capability_kind, layer, "Rescue people", None)
.unwrap();
let extension = p
.add_element(capability_kind, layer, "Rescue in heavy weather", None)
.unwrap();
let actor = p
.add_element(participant_kind, layer, "Rescue operator", None)
.unwrap();
let mut app = App::with_project(p);
send(&mut app, Message::Select(mission));
send(&mut app, Message::AddRelation);
assert!(
matches!(app.workflow.as_ref(), Some(Workflow::Relation(d)) if d.kind == RelationKind::MissionCapability && d.source.as_ref().unwrap().id == mission)
);
send(
&mut app,
Message::Workflow(Event::Target(crate::app::Choice {
id: base,
label: "Rescue people".into(),
})),
);
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.project().relations.len(), 1);
assert!(
app.project()
.relations
.values()
.any(|r| r.name == "Protect life exploits Rescue people")
);
send(&mut app, Message::Select(mission));
send(
&mut app,
Message::AddRelationKind(RelationKind::MissionInvolvement, true),
);
assert!(
matches!(app.workflow.as_ref(), Some(Workflow::Relation(d)) if d.target.as_ref().unwrap().id == mission && d.source.is_none())
);
send(
&mut app,
Message::Workflow(Event::Source(crate::app::Choice {
id: actor,
label: "Rescue operator".into(),
})),
);
send(&mut app, Message::Workflow(Event::Apply));
let before = app.project().clone();
send(&mut app, Message::Select(extension));
send(
&mut app,
Message::AddRelationKind(RelationKind::CapabilityExtend, false),
);
send(
&mut app,
Message::Workflow(Event::Target(crate::app::Choice {
id: base,
label: "Rescue people".into(),
})),
);
let condition = "Sea state >= 6\nAND crew is qualified";
send(
&mut app,
Message::Workflow(Event::Condition(text_editor::Action::Edit(
text_editor::Edit::Paste(std::sync::Arc::new(condition.into())),
))),
);
assert_eq!(
*app.project(),
before,
"preview must preserve committed model"
);
send(&mut app, Message::Workflow(Event::Apply));
assert!(app.project().relations.values().any(|r| matches!(&r.body, RelationBody::CapabilityExtend {source,target,condition: value} if *source == extension && *target == base && value == condition)));
assert_eq!(
crate::source_workspace::compile(&app.source_workspace.bundle())
.unwrap()
.decoded
.project,
*app.project()
);
send(&mut app, Message::Undo);
assert_eq!(*app.project(), before);
send(&mut app, Message::Redo);
assert_eq!(app.project().relations.len(), 3);
}
}
#[test]
fn property_enum_and_optional_reference_roundtrip() {
let mut p = Project::empty();
let item = p
.add_element(Kind::DataType, Layer::Oa, "Alarm severity", None)
.unwrap();
let mut d = Draft::from_project(&p, item).unwrap();
d.shape(
"/body/Oa/Shared/DataType/definition",
json!({"Enumeration":{"literals":["minor","major"]}}),
)
.unwrap();
let p = execute_batch(&p, &[d.command().unwrap()]).unwrap();
assert_eq!(language::parse(&language::format(&p)).unwrap(), p);
}
#[test]
fn modal_owns_background_commands_and_shortcuts() {
let mut app = App::with_project(fixture());
send(&mut app, Message::AddRelation);
let before = app.project().clone();
send(&mut app, Message::Layer(Layer::Sa));
send(&mut app, Message::Page(Page::Text));
send(&mut app, Message::ShortcutUndo(false));
send(&mut app, Message::Save(false));
assert_eq!(app.layer, Layer::Oa);
assert_eq!(app.page, Page::Diagram);
assert!(!app.busy);
assert_eq!(*app.project(), before);
send(&mut app, Message::Escape);
assert!(app.workflow.is_none());
}
#[test]
fn selecting_current_constructors_preserves_payloads() {
let mut app = App::with_project(Project::demo());
let before = app.project().clone();
for e in before.elements.values() {
send(&mut app, Message::Select(e.id));
send(&mut app, Message::ElementKind(e.kind()));
assert!(!app.draft_dirty());
}
for r in before.relations.values() {
send(&mut app, Message::Select(r.id));
send(&mut app, Message::RelationKind(r.kind()));
assert!(!app.draft_dirty());
}
let mut p = Project::empty();
let id = p
.add_element(Kind::DataType, Layer::Oa, "Severity", None)
.unwrap();
let mut app = App::with_project(p);
send(&mut app, Message::Select(id));
let path = "/body/Oa/Shared/DataType/definition";
send(
&mut app,
Message::FieldVariant(
path.into(),
json!({"Enumeration":{"literals":["minor","major"]}}),
),
);
send(&mut app, Message::Apply);
let accepted = app.project().clone();
send(
&mut app,
Message::FieldVariant(path.into(), json!({"Enumeration":{"literals":[]}})),
);
send(&mut app, Message::Apply);
assert_eq!(*app.project(), accepted);
}
#[test]
fn dragging_keeps_semantic_selection_and_same_node_remains_editable() {
let mut app = App::with_project(fixture());
let diagram = app.active_diagram.unwrap();
let placement = app.project().diagrams[&diagram]
.nodes
.values()
.next()
.unwrap()
.clone();
let semantic = placement.element;
send(&mut app, Message::SelectNode(placement.id));
let before = app.project().clone();
for distance in [40., 80.] {
send(
&mut app,
Message::MoveNode(
placement.id,
Position {
x: placement.position.x + distance,
y: placement.position.y + distance,
},
),
);
assert_eq!(app.selected, semantic);
assert_eq!(app.selected_node, Some(placement.id));
assert_eq!(
app.draft.as_ref().unwrap().kind,
crate::forms::RecordKind::Element
);
send(&mut app, Message::SelectNode(placement.id));
assert_eq!(app.selected, semantic);
}
send(&mut app, Message::Undo);
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before);
// Explicit placement inspection must not make its canvas card unselectable.
send(&mut app, Message::Select(placement.id));
send(&mut app, Message::SelectNode(placement.id));
assert_eq!(app.selected, semantic);
send(
&mut app,
Message::Field("/name".into(), "Still editable after dragging".into()),
);
send(&mut app, Message::SelectNode(placement.id));
assert_eq!(
app.draft.as_ref().unwrap().inputs["/name"].raw,
"Still editable after dragging"
);
send(&mut app, Message::Apply);
assert_eq!(
app.project().elements[&semantic].name,
"Still editable after dragging"
);
}
#[test]
fn placement_identity_edit_then_drag_and_diagram_identity_then_arrange() {
let mut app = App::with_project(fixture());
let diagram = app.active_diagram.unwrap();
let node = *app.project().diagrams[&diagram]
.nodes
.keys()
.next()
.unwrap();
send(&mut app, Message::Select(node));
let changed = Id::new_v4();
send(&mut app, Message::Field("/id".into(), changed.to_string()));
send(&mut app, Message::SelectNode(node));
send(
&mut app,
Message::MoveNode(node, Position { x: 200., y: 150. }),
);
assert_eq!(
app.project().diagrams[&diagram].nodes[&changed].position,
Position { x: 200., y: 150. }
);
send(&mut app, Message::Select(diagram));
let changed_diagram = Id::new_v4();
send(
&mut app,
Message::Field("/id".into(), changed_diagram.to_string()),
);
send(&mut app, Message::Command(Command::Layout(diagram)));
assert_eq!(app.active_diagram, Some(changed_diagram));
assert!(!app.project().contains(diagram));
}
#[test]
fn placement_relocation_follows_cross_layer_traceability_view() {
let mut p = fixture();
let original = p.ensure_diagram(Layer::Oa);
let target = p.ensure_diagram(Layer::Sa);
p.diagrams.get_mut(&target).unwrap().projection = ProjectionKind::Traceability;
let node = *p.diagrams[&original].nodes.keys().next().unwrap();
let mut app = App::with_project(p);
send(&mut app, Message::Select(node));
send(
&mut app,
Message::Field("/diagram".into(), target.to_string()),
);
assert!(app.apply(), "{}", app.status);
assert_eq!(app.layer, Layer::Sa);
assert_eq!(app.active_diagram, Some(target));
assert_eq!(app.selected_node, Some(node));
assert!(app.project().diagrams[&target].nodes.contains_key(&node));
assert!(!app.project().diagrams[&original].nodes.contains_key(&node));
send(&mut app, Message::Select(target));
send(&mut app, Message::Field("/layer".into(), "Oa".into()));
assert!(app.apply(), "{}", app.status);
assert_eq!(app.layer, Layer::Oa);
assert_eq!(app.active_diagram, Some(target));
}
#[test]
fn manual_trace_requires_target_and_selection_normalizes_creation_kind() {
let p = Project::demo();
let a = p
.elements
.values()
.find(|e| e.kind() == Kind::Activity)
.unwrap()
.id;
let b = p
.elements
.values()
.find(|e| e.kind() == Kind::Function)
.unwrap()
.id;
let mut app = App::with_project(p.clone());
send(&mut app, Message::Select(b));
assert_eq!(app.create_kind, Kind::Function);
send(&mut app, Message::AddTrace);
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(*app.project(), p);
send(
&mut app,
Message::Workflow(Event::Target(crate::app::Choice {
id: a,
label: "OA".into(),
})),
);
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.project().traces.len(), p.traces.len() + 1);
send(&mut app, Message::Select(a));
assert_eq!(app.create_kind, Kind::Activity);
}
#[test]
fn invalid_save_and_continue_returns_to_repairable_draft() {
let mut app = App::with_project(fixture());
source(&mut app, "arcadia 2; ?");
send(&mut app, Message::Request(Pending::New));
send(&mut app, Message::SaveAndContinue);
assert!(app.pending.is_none());
assert_eq!(app.page, Page::Text);
send(&mut app, Message::RevertSource);
assert!(!app.source_dirty());
}
#[test]
fn counterpart_creation_focuses_visible_target_and_reuse_keeps_views() {
let mut app = App::with_project(fixture());
let source = app
.project()
.elements
.values()
.find(|e| e.kind() == Kind::Activity)
.unwrap()
.id;
send(&mut app, Message::Select(source));
send(&mut app, Message::StartTransition);
send(
&mut app,
Message::Workflow(Event::Scope("Selected only".into())),
);
send(&mut app, Message::Workflow(Event::Apply));
let target = app.selected;
assert_eq!(app.project().elements[&target].kind(), Kind::Function);
assert!(
app.project().diagrams[&app.active_diagram.unwrap()]
.nodes
.values()
.any(|n| n.element == target)
);
let before = app.project().clone();
send(&mut app, Message::StartTransition);
send(
&mut app,
Message::Workflow(Event::Scope("Selected only".into())),
);
send(&mut app, Message::Workflow(Event::Apply));
assert_eq!(app.project().diagrams, before.diagrams);
assert_eq!(app.selected, source);
assert!(
app.project().diagrams[&app.active_diagram.unwrap()]
.nodes
.values()
.any(|n| n.element == source)
);
}
#[test]
fn transition_resolutions_belong_to_the_effective_target_and_kind() {
use oa_core::transitions::{FieldDecision, TransitionField};
fn transition(workflow: &Workflow) -> &crate::workflow::Transition {
let Workflow::Transition(transition) = workflow else {
panic!("Transition workflow")
};
transition
}
let mut p = Project::empty();
let source = p
.add_element(Kind::Activity, Layer::Oa, "Source work", None)
.unwrap();
let first = p
.add_element(Kind::Function, Layer::Sa, "First target", None)
.unwrap();
let second = p
.add_element(Kind::Function, Layer::Sa, "Second target", None)
.unwrap();
let trace = Id::new_v4();
p.traces.insert(
trace,
Trace {
id: trace,
alias: "work_trace".into(),
name: "Work trace".into(),
description: String::new(),
oa: source,
sa: first,
kind: TraceKind::Refinement,
baseline: None,
},
);
let mut workflow = Workflow::transition(&p, source).unwrap();
workflow
.update(
&p,
Event::Resolve(
source,
TransitionField::Name,
FieldDecision::Value("Merged for first target".into()),
),
)
.unwrap();
assert!(
transition(&workflow).request.mappings.is_empty(),
"First target was inferred"
);
let choose = |id| crate::app::Choice {
id,
label: String::new(),
};
workflow
.update(&p, Event::MappingTarget(source, choose(first)))
.unwrap();
assert_eq!(
transition(&workflow).request.resolutions.len(),
1,
"Same inferred target preserves merged draft"
);
workflow
.update(&p, Event::MappingTarget(source, choose(second)))
.unwrap();
assert!(transition(&workflow).request.resolutions.is_empty());
assert!(
transition(&workflow)
.plan
.conflicts
.iter()
.any(|c| c.source == Some(source)
&& c.target == Some(second)
&& c.field == Some(TransitionField::Name))
);
assert!(
workflow.update(&p, Event::Apply).is_err(),
"Old resolution cannot overwrite second target"
);
workflow
.update(
&p,
Event::Resolve(
source,
TransitionField::Name,
FieldDecision::Value("Reviewed second target".into()),
),
)
.unwrap();
workflow
.update(&p, Event::MappingTarget(source, choose(second)))
.unwrap();
assert_eq!(transition(&workflow).request.resolutions.len(), 1);
let accepted = workflow.update(&p, Event::Apply).unwrap().unwrap();
assert_eq!(accepted.elements[&second].name, "Reviewed second target");
assert_eq!(accepted.elements[&first].name, "First target");
workflow
.update(&p, Event::MappingTarget(source, choose(Id::nil())))
.unwrap();
assert!(
transition(&workflow).request.resolutions.is_empty(),
"Creation is a different target context"
);
workflow
.update(
&p,
Event::Resolve(
source,
TransitionField::Description,
FieldDecision::Value("Draft for inferred function kind".into()),
),
)
.unwrap();
workflow
.update(&p, Event::MappingKind(source, Kind::Function))
.unwrap();
assert_eq!(
transition(&workflow).request.resolutions.len(),
1,
"Same inferred kind preserves draft"
);
let participant = p
.add_element(Kind::Entity, Layer::Oa, "Participant", None)
.unwrap();
let mut kinds = Workflow::transition(&p, participant).unwrap();
kinds
.update(&p, Event::MappingKind(participant, Kind::SystemActor))
.unwrap();
kinds
.update(
&p,
Event::Resolve(
participant,
TransitionField::Description,
FieldDecision::Value("Actor review".into()),
),
)
.unwrap();
kinds
.update(&p, Event::MappingKind(participant, Kind::SystemActor))
.unwrap();
assert_eq!(transition(&kinds).request.resolutions.len(), 1);
kinds
.update(&p, Event::MappingKind(participant, Kind::System))
.unwrap();
assert!(
transition(&kinds).request.resolutions.is_empty(),
"Changed kind requires a new decision"
);
}
#[test]
fn behavior_and_data_authoring_do_not_place_nodes_in_unrelated_architecture_views() {
let p = Project::demo();
let scenario = p
.elements
.values()
.find(|e| e.layer() == Layer::Sa && e.kind() == Kind::Scenario)
.unwrap()
.id;
let mut app = App::with_project(p);
send(&mut app, Message::Layer(Layer::Sa));
send(&mut app, Message::Page(Page::Behavior));
let diagrams = app.project().diagrams.clone();
send(&mut app, Message::Select(scenario));
send(&mut app, Message::CreateKind(Kind::Occurrence));
send(&mut app, Message::Add(true));
let fragment = app.selected;
send(
&mut app,
Message::FieldVariant(
"/body/Sa/Shared/Occurrence/body".into(),
json!({"Fragment":{"operator":"Optional","branches":[]}}),
),
);
assert!(app.apply(), "{}", app.status);
send(&mut app, Message::Select(fragment));
send(&mut app, Message::Add(true));
let branch = app.selected;
send(&mut app, Message::Select(branch));
send(&mut app, Message::Add(true));
let occurrence = app.selected;
assert_eq!(app.project().elements[&fragment].parent, Some(scenario));
assert!(
app.project().elements[&scenario]
.steps()
.unwrap()
.contains(&fragment)
);
assert!(matches!(app.project().elements[&fragment].shared(),
Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. })
if branches == &vec![branch]));
assert_eq!(app.project().elements[&occurrence].parent, Some(branch));
assert_eq!(
app.project().elements[&branch].steps().unwrap(),
&vec![occurrence]
);
assert_eq!(app.project().diagrams, diagrams);
send(&mut app, Message::Page(Page::Data));
send(&mut app, Message::CreateKind(Kind::ExchangeItem));
send(&mut app, Message::Add(false));
assert_eq!(
app.project().elements[&app.selected].kind(),
Kind::ExchangeItem
);
assert_eq!(app.project().diagrams, diagrams);
send(&mut app, Message::Page(Page::Diagram));
let active = app.active_diagram.unwrap();
send(&mut app, Message::CreateKind(Kind::Function));
send(&mut app, Message::Add(false));
assert_eq!(
app.project().diagrams[&active].nodes.len(),
diagrams[&active].nodes.len() + 1
);
assert!(
app.project().diagrams[&active]
.nodes
.values()
.any(|node| node.element == app.selected)
);
for (id, before) in &diagrams {
if *id != active {
assert_eq!(&app.project().diagrams[id], before);
}
}
}
#[test]
fn revealing_an_implicit_function_port_unfolds_its_visible_architecture_owner() {
let mut p = Project::demo();
let diagram = p
.diagrams
.values()
.find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Allocation)
.unwrap()
.id;
let view = oa_core::projection::project_view(&p, diagram);
let function = view
.nodes
.iter()
.find(|n| p.elements[&n.element].kind() == Kind::Function && n.visual_parent.is_some())
.unwrap();
let ancestor = function.visual_parent.unwrap();
let port = p
.children(function.element)
.find(|e| e.kind() == Kind::Port)
.unwrap()
.id;
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&ancestor)
.unwrap()
.collapsed = true;
assert!(crate::navigation::diagrams_containing(&p, port).contains(&diagram));
let mut app = App::with_project(p);
send(&mut app, Message::RevealInDiagram(diagram, port));
assert_eq!(app.selected, port);
assert_eq!(app.active_diagram, Some(diagram));
assert!(!app.project().diagrams[&diagram].nodes[&ancestor].collapsed);
assert!(
oa_core::projection::project_view(app.project(), diagram)
.ports
.iter()
.any(|p| p.element == port)
);
}
#[test]
fn creation_from_capabilities_routes_work_to_architecture_and_folded_allocation_inserts_below_existing_work()
{
let p = Project::demo();
let system = p
.elements
.values()
.find(|e| e.kind() == Kind::System)
.unwrap()
.id;
let capabilities = p
.diagrams
.values()
.find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Capabilities)
.unwrap()
.id;
let mut app = App::with_project(p);
send(&mut app, Message::Layer(Layer::Sa));
send(
&mut app,
Message::Diagram(crate::app::Choice {
id: capabilities,
label: String::new(),
}),
);
send(&mut app, Message::CreateKind(Kind::Function));
send(&mut app, Message::Add(false));
let diagram = app.active_diagram.unwrap();
assert_eq!(
app.project().diagrams[&diagram].projection,
ProjectionKind::Allocation
);
assert!(
app.project().diagrams[&diagram]
.nodes
.values()
.any(|n| n.element == app.selected)
);
send(&mut app, Message::Select(system));
let owner = app.selected_node.unwrap();
send(&mut app, Message::ToggleCollapse);
assert!(app.project().diagrams[&diagram].nodes[&owner].collapsed);
assert_eq!(
app.selected, system,
"Folding must retain semantic selection"
);
let before = app.project().clone();
send(&mut app, Message::Add(true));
let created = app.selected;
let view = oa_core::projection::project_view(app.project(), diagram);
let child = view
.nodes
.iter()
.find(|n| n.element == created)
.unwrap_or_else(|| panic!("Created work is not visible: {}", app.status));
assert_eq!(child.visual_parent, Some(owner));
assert!(!app.project().diagrams[&diagram].nodes[&owner].collapsed);
assert!(
view.nodes
.iter()
.filter(|n| n.visual_parent == Some(owner) && n.id != child.id)
.all(|n| n.position.y + n.size.height <= child.position.y)
);
let parent = view.nodes.iter().find(|n| n.id == owner).unwrap();
assert!(
view.nodes
.iter()
.filter(|n| n.visual_parent.is_none() && n.id != owner)
.all(|n| n.position.x + n.size.width <= parent.position.x
|| parent.position.x + parent.size.width <= n.position.x
|| n.position.y + n.size.height <= parent.position.y
|| parent.position.y + parent.size.height <= n.position.y)
);
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before);
}
#[test]
fn nested_allocation_insertion_reflows_sibling_subtrees_and_preserves_fold_state() {
use oa_core::projection::{project_diagram, project_view};
for folded in [false, true] {
let mut p = Project::empty();
let root = p
.add_element(Kind::Entity, Layer::Oa, "Root", None)
.unwrap();
let a = p
.add_element(Kind::Entity, Layer::Oa, "A", Some(root))
.unwrap();
let b = p
.add_element(Kind::Entity, Layer::Oa, "B", Some(root))
.unwrap();
let other = p
.add_element(Kind::Entity, Layer::Oa, "Other root", None)
.unwrap();
let a_work = p
.add_element(Kind::Activity, Layer::Oa, "A work", None)
.unwrap();
let b_work = p
.add_element(Kind::Activity, Layer::Oa, "B work", None)
.unwrap();
// A preserved OA boundary remains explicit Legacy content in schema3.
let b_port = Id::new_v4();
p.elements.insert(
b_port,
oa_core::Element {
id: b_port,
alias: "b_boundary".into(),
name: "B boundary".into(),
description: String::new(),
parent: Some(b),
body: ElementBody::shared_in(
Layer::Oa,
SharedBody::Port {
contract: PortContract::Legacy,
legacy: Some(LegacyPortContract {
direction: PortDirection::InOut,
interface: None,
}),
},
),
},
);
p.relate(RelationKind::Allocation, "A performs", a, a_work);
p.relate(RelationKind::Allocation, "B performs", b, b_work);
let diagram = p.ensure_diagram(Layer::Oa);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
for id in [root, a, b, other, a_work, b_work] {
p.place(diagram, id, Position { x: 60., y: 60. });
}
let mut p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
let view = project_view(&p, diagram);
let root_node = view.nodes.iter().find(|n| n.element == root).unwrap();
let a_node = view.nodes.iter().find(|n| n.element == a).unwrap();
let b_node = view.nodes.iter().find(|n| n.element == b).unwrap();
// Layout's UUID order is intentionally irrelevant to this scenario:
// explicitly put A before B, as a user can arrange these participants.
p = execute_batch(
&p,
&[
Command::MoveNode {
diagram,
node: a_node.id,
position: Position {
x: root_node.position.x + 24.,
y: root_node.position.y + 64.,
},
with_descendants: true,
},
Command::MoveNode {
diagram,
node: b_node.id,
position: Position {
x: root_node.position.x + 24.,
y: root_node.position.y + 64. + a_node.size.height + 16.,
},
with_descendants: true,
},
],
)
.unwrap();
let view = project_view(&p, diagram);
let root_node = view.nodes.iter().find(|n| n.element == root).unwrap();
let other_node = view.nodes.iter().find(|n| n.element == other).unwrap().id;
let b_node = view.nodes.iter().find(|n| n.element == b).unwrap().id;
let b_work_node = view.nodes.iter().find(|n| n.element == b_work).unwrap().id;
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&other_node)
.unwrap()
.position = Position {
x: root_node.position.x,
y: root_node.position.y + root_node.size.height + 24.,
};
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&b_node)
.unwrap()
.collapsed = folded;
// Deliberately overlapping unrelated roots must not be rearranged.
let mut unrelated = Vec::new();
for name in ["Unrelated one", "Unrelated two"] {
let id = p.add_element(Kind::Entity, Layer::Oa, name, None).unwrap();
unrelated.push(p.place(diagram, id, Position { x: 2000., y: 1000. }));
}
let before = p.clone();
let mut expanded = p.diagrams[&diagram].clone();
expanded.nodes.get_mut(&b_node).unwrap().collapsed = false;
let before_view = project_diagram(&p, &expanded);
let before_port = before_view
.ports
.iter()
.find(|p| p.element == b_port)
.unwrap()
.position;
let mut app = App::with_project(p);
send(&mut app, Message::Select(a));
send(&mut app, Message::CreateKind(Kind::Activity));
send(&mut app, Message::Add(true));
let created = app.selected;
assert_eq!(
app.project().elements[&created].kind(),
Kind::Activity,
"{}",
app.status
);
let after = &app.project().diagrams[&diagram];
assert_eq!(after.nodes[&b_node].collapsed, folded);
let dy =
after.nodes[&b_node].position.y - before.diagrams[&diagram].nodes[&b_node].position.y;
assert!(dy > 0., "B must move below the enlarged A");
assert_eq!(
after.nodes[&b_work_node].position.y,
before.diagrams[&diagram].nodes[&b_work_node].position.y + dy
);
assert!(
after.nodes[&other_node].position.y
> before.diagrams[&diagram].nodes[&other_node].position.y
);
for id in unrelated {
assert_eq!(after.nodes[&id], before.diagrams[&diagram].nodes[&id]);
}
let mut expanded = after.clone();
expanded.nodes.get_mut(&b_node).unwrap().collapsed = false;
let view = project_diagram(app.project(), &expanded);
let after_port = view
.ports
.iter()
.find(|p| p.element == b_port)
.unwrap()
.position;
assert_eq!(
after_port,
Position {
x: before_port.x,
y: before_port.y + dy
}
);
for (left, right) in [(a, b), (root, other), (a_work, created)] {
let left = view.nodes.iter().find(|n| n.element == left).unwrap();
let right = view.nodes.iter().find(|n| n.element == right).unwrap();
assert!(left.position.y + left.size.height <= right.position.y);
}
assert!(crate::diagram::insertion_adjustments(app.project(), diagram, created).is_empty());
send(&mut app, Message::Undo);
assert_eq!(
app.project(),
&before,
"Creation and all reflow must undo together"
);
if folded {
send(&mut app, Message::Select(root));
send(&mut app, Message::CreateKind(Kind::Entity));
send(&mut app, Message::Add(true));
let mut expanded = app.project().diagrams[&diagram].clone();
expanded.nodes.get_mut(&b_node).unwrap().collapsed = false;
let view = project_diagram(app.project(), &expanded);
let b = view.nodes.iter().find(|n| n.id == b_node).unwrap();
let created = view
.nodes
.iter()
.find(|n| n.element == app.selected)
.unwrap();
assert!(b.position.y + b.size.height <= created.position.y);
assert!(app.project().diagrams[&diagram].nodes[&b_node].collapsed);
}
}
}
#[test]
fn routing_actions_are_atomic_guarded_and_fit_the_authored_detour() {
use crate::route_edit::Event as Route;
let mut p = Project::empty();
let sender = p.add(Kind::Activity, "Report", 0., 0.);
let target = p.add(Kind::Activity, "Receive", 600., 0.);
let edge = p.relate(RelationKind::Interaction, "Report signal", sender, target);
let diagram = p.ensure_diagram(Layer::Oa);
let mut app = App::with_project(p.clone());
send(&mut app, Message::Select(edge));
send(&mut app, Message::Route(Route::AddBend));
assert_eq!(
app.project().diagrams[&diagram].routes[0].waypoints.len(),
1
);
let context = app.route_context().unwrap();
let mut route = app.project().diagrams[&diagram].routes[0].clone();
route.waypoints[0] = Position { x: 400., y: -700. };
send(
&mut app,
Message::Route(Route::Commit(context, route.clone(), Some(0))),
);
assert_eq!(app.project().diagrams[&diagram].routes[0], route);
send(&mut app, Message::Fit);
let pin = route.waypoints[0];
assert!(pin.y * app.zoom + app.pan.y >= 0.);
send(&mut app, Message::Route(Route::RemoveBend));
assert!(
app.project().diagrams[&diagram].routes[0]
.waypoints
.is_empty()
);
send(&mut app, Message::Undo);
assert_eq!(app.project().diagrams[&diagram].routes[0], route);
let accepted = app.project().clone();
send(
&mut app,
Message::Route(Route::Commit(context, route.clone(), Some(0))),
);
assert_eq!(
app.project(),
&accepted,
"A stale gesture must not overwrite newer routing"
);
let current = app.route_context().unwrap();
send(&mut app, Message::Escape);
send(
&mut app,
Message::Route(Route::Commit(current, route.clone(), Some(0))),
);
assert_eq!(
app.project(),
&accepted,
"Escape invalidates the gesture epoch"
);
send(&mut app, Message::Route(Route::Reset));
assert!(app.project().diagrams[&diagram].routes.is_empty());
send(&mut app, Message::Undo);
assert_eq!(app.project(), &accepted);
let current = app.route_context().unwrap();
source(&mut app, "invalid unsaved source");
send(
&mut app,
Message::Route(Route::Commit(current, route, Some(0))),
);
assert_eq!(app.project(), &accepted);
assert_eq!(app.source_editor.text(), "invalid unsaved source");
}
#[test]
fn adding_bend_between_close_ports_stays_outside_owner_clearance() {
let mut p = Project::empty();
let diagram = p.ensure_diagram(Layer::Sa);
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 edge = p.relate(RelationKind::FunctionalExchange, "Status", a, b);
p.place(diagram, a, Position { x: 0., y: 100. });
p.place(diagram, b, Position { x: 260., y: 100. });
let mut app = App::with_project(p);
send(&mut app, Message::Select(edge));
send(&mut app, Message::Route(crate::route_edit::Event::AddBend));
let p = app.project();
let view = projection::project_view(p, diagram);
let routed = routing::route_diagram(p, &p.diagrams[&diagram], &view);
assert!(
routed.edges[&edge].warnings.is_empty(),
"{:?}",
routed.edges[&edge].warnings
);
assert_eq!(
p.diagrams[&diagram].routes[0].waypoints,
vec![Position {
x: 242.,
y: view
.ports
.iter()
.find(|port| p.elements[&port.element].parent == Some(a))
.unwrap()
.position
.y
}]
);
}