Establish OA/SA prototype baseline and first-release work
This commit is contained in:
@@ -0,0 +1,938 @@
|
||||
use crate::{
|
||||
app::{App, 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
|
||||
}
|
||||
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((
|
||||
std::path::PathBuf::from("resized-model.arcadia"),
|
||||
project.clone(),
|
||||
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 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((
|
||||
std::path::PathBuf::from("measured.arcadia"),
|
||||
p.clone(),
|
||||
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 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!(
|
||||
language::parse(&app.source_editor.text()).unwrap(),
|
||||
*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!(language::parse(&app.source_editor.text()).unwrap(), 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!(
|
||||
language::parse(&app.source_editor.text()).unwrap(),
|
||||
*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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user