491 lines
18 KiB
Rust
491 lines
18 KiB
Rust
use super::*;
|
|
use crate::app::architecture_authoring::{Event, kinds};
|
|
|
|
fn setup(layer: Layer) -> App {
|
|
let mut app = App::with_project(Project::empty());
|
|
app.layer = layer;
|
|
app.active_diagram = None;
|
|
let _ = app.update(Message::AddDiagram);
|
|
app.zoom = 0.75;
|
|
app.pan = iced::Vector::new(37., -19.);
|
|
app
|
|
}
|
|
fn send(app: &mut App, event: Event) {
|
|
let _ = app.update(Message::Architecture(event));
|
|
}
|
|
#[test]
|
|
fn architecture_creation_names_before_commit_preserves_camera_and_undoes_once() {
|
|
let mut app = setup(Layer::Oa);
|
|
let before = app.document.bundle().clone();
|
|
let model = app.project().clone();
|
|
let camera = (app.zoom, app.pan);
|
|
send(&mut app, Event::Start(Kind::Entity, None));
|
|
assert_eq!(app.project(), &model);
|
|
let context = app.route_context().unwrap();
|
|
send(
|
|
&mut app,
|
|
Event::Click(context, Position { x: 219., y: 117. }),
|
|
);
|
|
assert_eq!(app.project(), &model);
|
|
assert!(app.architecture.as_ref().unwrap().inline());
|
|
send(&mut app, Event::Name(" Response team 雨 ".into()));
|
|
send(&mut app, Event::Submit);
|
|
assert!(app.architecture.is_none());
|
|
assert_eq!((app.zoom, app.pan), camera);
|
|
let element = &app.project().elements[&app.selected];
|
|
assert_eq!(element.name, " Response team 雨 ");
|
|
assert_eq!(element.parent, None);
|
|
let node =
|
|
&app.project().diagrams[&app.active_diagram.unwrap()].nodes[&app.selected_node.unwrap()];
|
|
assert_eq!(node.position, Position { x: 219., y: 117. });
|
|
let accepted = app.document.bundle().clone();
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &accepted);
|
|
}
|
|
#[test]
|
|
fn architecture_context_choices_are_small_and_system_is_singleton() {
|
|
let mut app = setup(Layer::Oa);
|
|
assert_eq!(kinds(&app), vec![Kind::Entity, Kind::Actor, Kind::Activity]);
|
|
app = setup(Layer::Sa);
|
|
assert_eq!(
|
|
kinds(&app),
|
|
vec![Kind::System, Kind::SystemActor, Kind::Function]
|
|
);
|
|
send(
|
|
&mut app,
|
|
Event::Start(Kind::System, Some(Position { x: 60., y: 60. })),
|
|
);
|
|
send(&mut app, Event::Name("Pump controller".into()));
|
|
send(&mut app, Event::Submit);
|
|
assert_eq!(kinds(&app), vec![Kind::SystemActor, Kind::Function]);
|
|
}
|
|
#[test]
|
|
fn architecture_name_survives_rejected_click_resize_navigation_and_escape_is_explicit() {
|
|
let mut app = setup(Layer::Oa);
|
|
let before = app.document.bundle().clone();
|
|
send(&mut app, Event::Start(Kind::Activity, None));
|
|
send(&mut app, Event::Name("Diagnose pressure".into()));
|
|
send(&mut app, Event::Start(Kind::Entity, None));
|
|
assert!(
|
|
matches!(&app.architecture.as_ref().unwrap().saved.intent, architecture_authoring::Intent::Create { name, kind: Kind::Activity, .. } if name == "Diagnose pressure")
|
|
);
|
|
send(&mut app, Event::Submit);
|
|
let mut stale = app.route_context().unwrap();
|
|
stale.revision += 1;
|
|
send(&mut app, Event::Click(stale, Position { x: 1., y: 2. }));
|
|
let _ = app.update(Message::Page(Page::Home));
|
|
let _ = app.update(Message::WindowResized(iced::Size::new(1100., 700.)));
|
|
assert_eq!(app.page, Page::Diagram);
|
|
assert!(app.architecture.is_some());
|
|
assert_eq!(app.document.bundle(), &before);
|
|
let _ = app.update(Message::Escape);
|
|
assert!(app.architecture.is_none());
|
|
assert_eq!(app.document.bundle(), &before);
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_existing_inventory_is_contextual_and_placement_never_implies_allocation() {
|
|
let mut app = setup(Layer::Oa);
|
|
let entity = app
|
|
.project()
|
|
.make_element(Kind::Entity, Layer::Oa, "Response team", None)
|
|
.unwrap();
|
|
let entity_id = entity.id;
|
|
let work = app
|
|
.project()
|
|
.make_element(Kind::Activity, Layer::Oa, "Report status", None)
|
|
.unwrap();
|
|
let work_id = work.id;
|
|
let data = app
|
|
.project()
|
|
.make_element(Kind::DataType, Layer::Oa, "Pressure", None)
|
|
.unwrap();
|
|
let data_id = data.id;
|
|
assert!(app.execute(Command::Batch(vec![
|
|
Command::CreateElement(entity),
|
|
Command::CreateElement(work),
|
|
Command::CreateElement(data)
|
|
])));
|
|
send(&mut app, Event::Existing(Some(Position { x: 80., y: 80. })));
|
|
let ids: Vec<_> = architecture_authoring::candidates(&app)
|
|
.iter()
|
|
.map(|row| row.0)
|
|
.collect();
|
|
assert!(ids.contains(&entity_id) && ids.contains(&work_id));
|
|
assert!(!ids.contains(&data_id));
|
|
send(&mut app, Event::Pick(entity_id));
|
|
let semantic = app.project().elements.clone();
|
|
let before = app.document.bundle().clone();
|
|
let camera = (app.zoom, app.pan);
|
|
send(
|
|
&mut app,
|
|
Event::Existing(Some(Position { x: 90., y: 100. })),
|
|
);
|
|
assert!(
|
|
!architecture_authoring::candidates(&app)
|
|
.iter()
|
|
.any(|row| row.0 == entity_id)
|
|
);
|
|
send(&mut app, Event::ShowPlaced(true));
|
|
assert!(
|
|
architecture_authoring::candidates(&app)
|
|
.iter()
|
|
.any(|row| row.0 == entity_id && row.2)
|
|
);
|
|
send(&mut app, Event::Pick(work_id));
|
|
assert_eq!(app.project().elements, semantic);
|
|
assert!(app.project().relations.is_empty());
|
|
assert_eq!((app.zoom, app.pan), camera);
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_checkpoint_retains_exact_name_point_and_fresh_binding_without_commands() {
|
|
for (name, placing) in [(" Team 雨 ", true), (" ", false)] {
|
|
let mut app = setup(Layer::Oa);
|
|
send(&mut app, Event::Start(Kind::Entity, None));
|
|
send(&mut app, Event::Name(name.into()));
|
|
if placing {
|
|
send(&mut app, Event::Submit);
|
|
}
|
|
app.architecture.as_mut().unwrap().saved.point = Some(Position { x: 127.5, y: 215. });
|
|
app.architecture.as_mut().unwrap().saved.placing = placing;
|
|
let snapshot = crate::recovery::Snapshot::capture(
|
|
app.canvas_document,
|
|
&app.document,
|
|
&app.source_workspace,
|
|
app.draft.as_ref(),
|
|
&app.behavior,
|
|
None,
|
|
app.selected,
|
|
)
|
|
.unwrap()
|
|
.with_architecture(app.architecture.as_ref());
|
|
let bytes = serde_json::to_vec(&snapshot).unwrap();
|
|
let decoded: crate::recovery::Snapshot = serde_json::from_slice(&bytes).unwrap();
|
|
let accepted = app.document.bundle().clone();
|
|
let mut restored = setup(Layer::Sa);
|
|
let _ = restored.update(Message::CheckpointLoaded(Ok(Arc::new(decoded))));
|
|
assert_eq!(restored.document.bundle(), &accepted);
|
|
let draft = &restored.architecture.as_ref().unwrap().saved;
|
|
assert!(
|
|
matches!(&draft.intent, architecture_authoring::Intent::Create { name: actual, .. } if actual == name)
|
|
);
|
|
assert_eq!(draft.placing, placing);
|
|
assert_eq!(draft.point, Some(Position { x: 127.5, y: 215. }));
|
|
if placing {
|
|
let context = restored.route_context().unwrap();
|
|
send(
|
|
&mut restored,
|
|
Event::Click(context, Position { x: 20., y: 30. }),
|
|
);
|
|
assert!(restored.architecture.as_ref().unwrap().inline());
|
|
send(&mut restored, Event::Submit);
|
|
assert!(restored.architecture.is_none());
|
|
assert_eq!(restored.project().elements[&restored.selected].name, name);
|
|
} else {
|
|
send(&mut restored, Event::Submit);
|
|
assert!(restored.architecture.is_some());
|
|
assert_eq!(restored.document.bundle(), &accepted);
|
|
}
|
|
let mut value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
|
value["architecture"]["base"] = json!("stale");
|
|
assert!(
|
|
serde_json::from_value::<crate::recovery::Snapshot>(value)
|
|
.unwrap()
|
|
.restore()
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan() {
|
|
use iced::widget::canvas::Program;
|
|
let mut app = setup(Layer::Oa);
|
|
send(&mut app, Event::Start(Kind::Actor, None));
|
|
send(&mut app, Event::Name("Control centre".into()));
|
|
send(&mut app, Event::Submit);
|
|
let bounds = iced::Rectangle {
|
|
x: 260.,
|
|
y: 180.,
|
|
width: 600.,
|
|
height: 380.,
|
|
};
|
|
let local = iced::Point::new(190., 100.);
|
|
let expected = Position {
|
|
x: (local.x - app.pan.x) / app.zoom,
|
|
y: (local.y - app.pan.y) / app.zoom,
|
|
};
|
|
let canvas = crate::diagram::DiagramCanvas {
|
|
project: app.project(),
|
|
view_index: None,
|
|
diagram: app.active_diagram.unwrap(),
|
|
selected: app.selected,
|
|
navigation_focus: None,
|
|
zoom: app.zoom,
|
|
pan: app.pan,
|
|
context: app.route_context().unwrap(),
|
|
selected_bend: None,
|
|
editable: true,
|
|
placing: true,
|
|
connection: None,
|
|
};
|
|
let mut state = crate::diagram::State::default();
|
|
let action = canvas
|
|
.update(
|
|
&mut state,
|
|
&iced::widget::canvas::Event::Mouse(iced::mouse::Event::ButtonPressed(
|
|
iced::mouse::Button::Left,
|
|
)),
|
|
bounds,
|
|
iced::mouse::Cursor::Available(iced::Point::new(
|
|
bounds.x + local.x,
|
|
bounds.y + local.y,
|
|
)),
|
|
)
|
|
.unwrap();
|
|
let message = action
|
|
.into_inner()
|
|
.0
|
|
.expect("named placement emits a command");
|
|
let _ = app.update(message);
|
|
assert_eq!(
|
|
app.architecture.as_ref().unwrap().saved.point,
|
|
Some(expected)
|
|
);
|
|
send(&mut app, Event::Submit);
|
|
let node =
|
|
&app.project().diagrams[&app.active_diagram.unwrap()].nodes[&app.selected_node.unwrap()];
|
|
assert_eq!(node.position, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_cancelled_gesture_cannot_commit_a_later_named_draft() {
|
|
let mut app = setup(Layer::Oa);
|
|
send(&mut app, Event::Start(Kind::Entity, None));
|
|
send(&mut app, Event::Name("First".into()));
|
|
send(&mut app, Event::Submit);
|
|
let stale = app.route_context().unwrap();
|
|
send(&mut app, Event::Cancel);
|
|
send(&mut app, Event::Start(Kind::Actor, None));
|
|
send(&mut app, Event::Name("Second".into()));
|
|
send(&mut app, Event::Submit);
|
|
let before = app.document.bundle().clone();
|
|
send(&mut app, Event::Click(stale, Position { x: 1., y: 1. }));
|
|
assert_eq!(app.document.bundle(), &before);
|
|
assert!(app.architecture.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_f2_rename_preserves_identity_placement_and_atomic_history() {
|
|
use iced::{keyboard, mouse, widget::canvas::Program};
|
|
let mut app = setup(Layer::Oa);
|
|
send(
|
|
&mut app,
|
|
Event::Start(Kind::Entity, Some(Position { x: 200., y: 120. })),
|
|
);
|
|
send(&mut app, Event::Name("Response team".into()));
|
|
send(&mut app, Event::Submit);
|
|
let before = app.document.bundle().clone();
|
|
let element = app.selected;
|
|
let node = app.selected_node;
|
|
let diagrams = app.project().diagrams.clone();
|
|
let camera = (app.zoom, app.pan);
|
|
let canvas = crate::diagram::DiagramCanvas {
|
|
project: app.project(),
|
|
view_index: None,
|
|
diagram: app.active_diagram.unwrap(),
|
|
selected: element,
|
|
navigation_focus: None,
|
|
zoom: app.zoom,
|
|
pan: app.pan,
|
|
context: app.route_context().unwrap(),
|
|
selected_bend: None,
|
|
editable: true,
|
|
placing: false,
|
|
connection: None,
|
|
};
|
|
let bounds = iced::Rectangle::with_size(iced::Size::new(800., 600.));
|
|
let cursor = mouse::Cursor::Available(iced::Point::new(500., 400.));
|
|
let mut state = crate::diagram::State::default();
|
|
let key = iced::widget::canvas::Event::Keyboard(keyboard::Event::KeyPressed {
|
|
key: keyboard::Key::Named(keyboard::key::Named::F2),
|
|
modified_key: keyboard::Key::Named(keyboard::key::Named::F2),
|
|
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::F2),
|
|
location: keyboard::Location::Standard,
|
|
modifiers: keyboard::Modifiers::empty(),
|
|
text: None,
|
|
repeat: false,
|
|
});
|
|
assert!(canvas.update(&mut state, &key, bounds, cursor).is_none());
|
|
let _ = canvas.update(
|
|
&mut state,
|
|
&iced::widget::canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
|
bounds,
|
|
cursor,
|
|
);
|
|
let message = canvas
|
|
.update(&mut state, &key, bounds, cursor)
|
|
.expect("focused F2 opens the selected semantic name")
|
|
.into_inner()
|
|
.0
|
|
.unwrap();
|
|
let _ = app.update(message);
|
|
assert!(app.architecture.as_ref().unwrap().inline());
|
|
send(&mut app, Event::Name(" Emergency team 雨 ".into()));
|
|
assert_eq!(app.document.bundle(), &before);
|
|
send(&mut app, Event::Submit);
|
|
assert!(app.architecture.is_none());
|
|
assert_eq!((app.selected, app.selected_node), (element, node));
|
|
assert_eq!(
|
|
app.project().elements[&element].name,
|
|
" Emergency team 雨 "
|
|
);
|
|
assert_eq!(app.project().diagrams, diagrams);
|
|
assert_eq!((app.zoom, app.pan), camera);
|
|
let accepted = app.document.bundle().clone();
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &accepted);
|
|
let context = app.route_context().unwrap();
|
|
send(
|
|
&mut app,
|
|
Event::Rename(context, element, Position { x: 200., y: 120. }),
|
|
);
|
|
send(&mut app, Event::Name("Cancelled name".into()));
|
|
let _ = app.update(Message::Escape);
|
|
assert!(app.architecture.is_none());
|
|
assert_eq!(app.document.bundle(), &accepted);
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_contextual_allocation_creates_work_relation_and_node_in_one_undo() {
|
|
for (layer, performer_kind, work_kind) in [
|
|
(Layer::Oa, Kind::Entity, Kind::Activity),
|
|
(Layer::Sa, Kind::System, Kind::Function),
|
|
] {
|
|
let mut app = setup(layer);
|
|
send(
|
|
&mut app,
|
|
Event::Start(performer_kind, Some(Position { x: 20., y: 20. })),
|
|
);
|
|
send(&mut app, Event::Name("Control team".into()));
|
|
send(&mut app, Event::Submit);
|
|
let performer = app.selected;
|
|
let before = app.document.bundle().clone();
|
|
send(
|
|
&mut app,
|
|
Event::Allocate(work_kind, performer, Some(Position { x: 55., y: 90. })),
|
|
);
|
|
send(&mut app, Event::Name("Assess pressure".into()));
|
|
assert_eq!(app.document.bundle(), &before);
|
|
send(&mut app, Event::Submit);
|
|
assert!(app.architecture.is_none(), "{}", app.status);
|
|
let work = app.selected;
|
|
assert_eq!(app.project().elements[&work].parent, None);
|
|
assert!(
|
|
app.project()
|
|
.relations
|
|
.values()
|
|
.any(|r| r.kind() == RelationKind::Allocation
|
|
&& r.source() == performer
|
|
&& r.target() == work)
|
|
);
|
|
let node = &app.project().diagrams[&app.active_diagram.unwrap()].nodes
|
|
[&app.selected_node.unwrap()];
|
|
assert_eq!(node.position, Position { x: 55., y: 90. });
|
|
let accepted = app.document.bundle().clone();
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &accepted);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_rejected_submission_keeps_inline_name_visible_and_invalid_click_is_ignored() {
|
|
let mut app = setup(Layer::Oa);
|
|
send(&mut app, Event::Start(Kind::Entity, None));
|
|
let context = app.route_context().unwrap();
|
|
send(
|
|
&mut app,
|
|
Event::Click(
|
|
context,
|
|
Position {
|
|
x: f32::NAN,
|
|
y: 30.,
|
|
},
|
|
),
|
|
);
|
|
assert!(app.architecture.as_ref().unwrap().unnamed_tool());
|
|
send(&mut app, Event::Click(context, Position { x: 40., y: 30. }));
|
|
send(&mut app, Event::Name("Retained team".into()));
|
|
// A replaced document must not accept a queued draft submission.
|
|
app.canvas_document = Id::new_v4();
|
|
let before = app.document.bundle().clone();
|
|
send(&mut app, Event::Submit);
|
|
let draft = app.architecture.as_ref().unwrap();
|
|
assert!(draft.inline());
|
|
assert!(draft.error.is_some());
|
|
assert!(
|
|
matches!(&draft.saved.intent, architecture_authoring::Intent::Create { name, .. } if name == "Retained team")
|
|
);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
let _ = app.update(Message::Escape);
|
|
assert!(app.architecture.is_none());
|
|
assert_eq!(app.document.bundle(), &before);
|
|
}
|
|
|
|
#[test]
|
|
fn basic_transition_explicit_overview_focus_wins_over_an_already_populated_architecture() {
|
|
use crate::workflow::{Event as W, Workflow};
|
|
let mut p = Project::empty();
|
|
let work = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Report status", None)
|
|
.unwrap();
|
|
let architecture = p.ensure_diagram(Layer::Sa);
|
|
p.diagrams.get_mut(&architecture).unwrap().projection = ProjectionKind::Allocation;
|
|
let mut first = Workflow::transition(&p, work).unwrap();
|
|
let Workflow::Transition(t) = &first else {
|
|
unreachable!()
|
|
};
|
|
let target = t
|
|
.plan
|
|
.changes
|
|
.iter()
|
|
.find(|c| c.source == work)
|
|
.unwrap()
|
|
.target;
|
|
let mut p = first.update(&p, W::Apply).unwrap().unwrap();
|
|
let mut overview = p.diagrams[&architecture].clone();
|
|
overview.id = Id::new_v4();
|
|
overview.alias = "chosen_overview".into();
|
|
overview.name = "Chosen system overview".into();
|
|
overview.projection = ProjectionKind::Overview;
|
|
overview.nodes.clear();
|
|
let chosen = overview.id;
|
|
p.diagrams.insert(chosen, overview);
|
|
let placement = p.place(chosen, target, Position { x: 823., y: 417. });
|
|
let mut app = App::with_project(p);
|
|
app.selected = work;
|
|
app.active_diagram = Some(architecture);
|
|
app.refresh_draft();
|
|
let _ = app.update(Message::StartTransition);
|
|
let _ = app.update(Message::Workflow(W::Destination(Choice {
|
|
id: chosen,
|
|
label: "Chosen system overview".into(),
|
|
})));
|
|
let before = app.project().diagrams.clone();
|
|
let _ = app.update(Message::Workflow(W::Apply));
|
|
assert!(app.workflow.is_none(), "{}", app.status);
|
|
assert_eq!(app.active_diagram, Some(chosen));
|
|
assert_eq!(app.selected, target);
|
|
assert_eq!(app.selected_node, Some(placement));
|
|
assert_eq!(app.project().diagrams, before);
|
|
}
|