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

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);
}