Files
ARCADIA-Studio/crates/oa-desktop/src/tests.rs
T
sorokin.oleg.edu 36db9356cc Add bounded definition and reference navigation for current source drafts
Preserve loaded draft bytes and model history, reject stale workspace and input events, and keep captured Escape under modal priority. Verified 181 focused Windows tests and strict desktop Clippy on exact inputs4; earlier full737 scope and component/native preparation evidence are archived separately. Native navigation acceptance remains pending.
2026-09-11 11:23:20 -07:00

1668 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(&format!("arcadia {SCHEMA_VERSION};"))
);
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());
let ticket = app.source_reveal_ticket();
send(&mut app, Message::SourceViewport(ticket));
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 })
);
let ticket = app.source_reveal_ticket();
send(&mut app, Message::SourceViewport(ticket));
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
}]
);
}