379 lines
14 KiB
Rust
379 lines
14 KiB
Rust
use super::*;
|
|
|
|
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));
|
|
}
|
|
fn create(app: &mut App, kind: Kind, name: &str) -> Id {
|
|
send(app, Event::Start(kind, Some(Position { x: 120., y: 160. })));
|
|
send(app, Event::Name(name.into()));
|
|
send(app, Event::Submit);
|
|
assert!(app.architecture.is_none(), "{}", app.status);
|
|
app.selected
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_allocated_work_is_root_owned_and_one_exact_history_change() {
|
|
for (layer, participant, work) in [
|
|
(Layer::Oa, Kind::Entity, Kind::Activity),
|
|
(Layer::Sa, Kind::System, Kind::Function),
|
|
] {
|
|
let mut app = setup(layer);
|
|
let performer = create(&mut app, participant, "Control team");
|
|
let before = app.document.bundle().clone();
|
|
let camera = (app.zoom, app.pan);
|
|
let point = Position { x: 151., y: 221. };
|
|
send(&mut app, Event::Allocate(work, performer, Some(point)));
|
|
send(&mut app, Event::Name(" Report 雨 ".into()));
|
|
assert_eq!(app.document.bundle(), &before);
|
|
send(&mut app, Event::Submit);
|
|
assert!(app.architecture.is_none(), "{}", app.status);
|
|
let created = app.selected;
|
|
let element = &app.project().elements[&created];
|
|
assert_eq!(element.kind(), work);
|
|
assert_eq!(element.name, " Report 雨 ");
|
|
assert_eq!(element.parent, None);
|
|
assert!(app.project().relations.values().any(|r| {
|
|
r.kind() == RelationKind::Allocation && r.source() == performer && r.target() == created
|
|
}));
|
|
let d = &app.project().diagrams[&app.active_diagram.unwrap()];
|
|
assert_eq!(d.nodes[&app.selected_node.unwrap()].position, point);
|
|
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);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo() {
|
|
use iced::widget::canvas::{Event as CanvasEvent, Program};
|
|
use iced::{keyboard, mouse};
|
|
let mut app = setup(Layer::Oa);
|
|
let element = create(&mut app, Kind::Entity, "Original team");
|
|
let before = app.document.bundle().clone();
|
|
let diagrams = app.project().diagrams.clone();
|
|
let camera = (app.zoom, app.pan);
|
|
let bounds = iced::Rectangle::new(iced::Point::ORIGIN, iced::Size::new(600., 380.));
|
|
let canvas = crate::diagram::DiagramCanvas {
|
|
project: app.project(),
|
|
view_index: None,
|
|
diagram: app.active_diagram.unwrap(),
|
|
selected: element,
|
|
zoom: app.zoom,
|
|
pan: app.pan,
|
|
context: app.route_context().unwrap(),
|
|
selected_bend: None,
|
|
editable: true,
|
|
placing: false,
|
|
connection: None,
|
|
};
|
|
let mut state = crate::diagram::State::default();
|
|
let key = CanvasEvent::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, mouse::Cursor::Unavailable)
|
|
.is_none()
|
|
);
|
|
let _ = canvas.update(
|
|
&mut state,
|
|
&CanvasEvent::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
|
bounds,
|
|
mouse::Cursor::Available(iced::Point::new(150., 120.)),
|
|
);
|
|
let action = canvas
|
|
.update(&mut state, &key, bounds, mouse::Cursor::Unavailable)
|
|
.unwrap();
|
|
let message = action.into_inner().0.unwrap();
|
|
let _ = app.update(message);
|
|
assert!(app.architecture.as_ref().unwrap().inline());
|
|
assert!(matches!(&app.architecture.as_ref().unwrap().saved.intent,
|
|
Intent::Rename { element: id, name } if *id == element && name == "Original team"));
|
|
send(&mut app, Event::Name(" Renamed team 雨 ".into()));
|
|
assert_eq!(app.document.bundle(), &before);
|
|
send(&mut app, Event::Submit);
|
|
assert!(app.architecture.is_none(), "{}", app.status);
|
|
assert_eq!(app.project().elements[&element].name, " Renamed 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);
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_blank_names_and_escape_never_accept_placeholder_or_rename() {
|
|
let mut app = setup(Layer::Oa);
|
|
let element = create(&mut app, Kind::Actor, "Dispatcher");
|
|
let before = app.document.bundle().clone();
|
|
let context = app.route_context().unwrap();
|
|
for event in [
|
|
Event::Start(Kind::Activity, Some(Position { x: 220., y: 330. })),
|
|
Event::Rename(context, element, Position { x: 150., y: 200. }),
|
|
] {
|
|
// Every cancel changes the gesture context; use the current context for F2.
|
|
let event = match event {
|
|
Event::Rename(_, element, point) => {
|
|
Event::Rename(app.route_context().unwrap(), element, point)
|
|
}
|
|
event => event,
|
|
};
|
|
send(&mut app, event);
|
|
send(&mut app, Event::Name(" \t ".into()));
|
|
send(&mut app, Event::Submit);
|
|
assert!(app.architecture.as_ref().unwrap().inline());
|
|
assert!(app.architecture.as_ref().unwrap().error.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_invalid_click_cannot_poison_inline_or_checkpoint_geometry() {
|
|
let mut app = setup(Layer::Oa);
|
|
send(&mut app, Event::Start(Kind::Entity, None));
|
|
let before = app.document.bundle().clone();
|
|
let context = app.route_context().unwrap();
|
|
for point in [
|
|
Position { x: f32::NAN, y: 0. },
|
|
Position {
|
|
x: 0.,
|
|
y: f32::INFINITY,
|
|
},
|
|
] {
|
|
send(&mut app, Event::Click(context, point));
|
|
let draft = app.architecture.as_ref().unwrap();
|
|
assert!(draft.unnamed_tool());
|
|
assert!(validate_saved(app.project(), &draft.saved).is_ok());
|
|
assert_eq!(app.document.bundle(), &before);
|
|
}
|
|
send(&mut app, Event::Click(context, Position { x: 25., y: 30. }));
|
|
assert!(app.architecture.as_ref().unwrap().inline());
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_stale_submit_keeps_create_and_rename_visible_and_exact() {
|
|
for rename in [false, true] {
|
|
for stale_document in [false, true] {
|
|
let mut app = setup(Layer::Oa);
|
|
let element = create(&mut app, Kind::Entity, "Accepted team");
|
|
let point = Position {
|
|
x: 151.5,
|
|
y: 227.25,
|
|
};
|
|
let event = if rename {
|
|
Event::Rename(app.route_context().unwrap(), element, point)
|
|
} else {
|
|
Event::Start(Kind::Activity, Some(point))
|
|
};
|
|
send(&mut app, event);
|
|
send(
|
|
&mut app,
|
|
Event::Name(" Retained engineering name 雨 ".into()),
|
|
);
|
|
// Change only the binding. The accepted model/source must not be
|
|
// rewritten or a rejected draft silently rebound to the new context.
|
|
if stale_document {
|
|
app.architecture.as_mut().unwrap().document = Id::new_v4();
|
|
} else {
|
|
app.architecture.as_mut().unwrap().revision = app.document.revision() + 1;
|
|
}
|
|
let draft = app.architecture.as_ref().unwrap();
|
|
let saved = serde_json::to_value(&draft.saved).unwrap();
|
|
let binding = (draft.document, draft.revision);
|
|
let model = app.project().clone();
|
|
let source = app.document.bundle().clone();
|
|
let revision = app.document.revision();
|
|
let camera = (app.zoom, app.pan);
|
|
|
|
for _ in 0..2 {
|
|
send(&mut app, Event::Submit);
|
|
let draft = app.architecture.as_ref().unwrap();
|
|
assert!(
|
|
draft.inline(),
|
|
"rename={rename}, stale_document={stale_document}"
|
|
);
|
|
assert!(inline_editor(&app).is_some());
|
|
assert_eq!(draft.error.as_deref(), Some(app.status.as_str()));
|
|
assert!(app.status.contains("cancel and restart"));
|
|
assert_eq!(serde_json::to_value(&draft.saved).unwrap(), saved);
|
|
assert_eq!((draft.document, draft.revision), binding);
|
|
assert_eq!(app.project(), &model);
|
|
assert_eq!(app.document.bundle(), &source);
|
|
assert_eq!(app.document.revision(), revision);
|
|
assert_eq!((app.zoom, app.pan), camera);
|
|
}
|
|
let _ = app.update(Message::Escape);
|
|
assert!(app.architecture.is_none());
|
|
assert_eq!(app.project(), &model);
|
|
assert_eq!(app.document.bundle(), &source);
|
|
let fresh = create(&mut app, Kind::Actor, "Fresh context");
|
|
assert_eq!(app.project().elements[&fresh].name, "Fresh context");
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.project(), &model);
|
|
assert_eq!(app.document.bundle(), &source);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn key_event(named: iced::keyboard::key::Named) -> iced::Event {
|
|
use iced::keyboard;
|
|
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
|
key: keyboard::Key::Named(named),
|
|
modified_key: keyboard::Key::Named(named),
|
|
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Escape),
|
|
location: keyboard::Location::Standard,
|
|
modifiers: keyboard::Modifiers::empty(),
|
|
text: None,
|
|
repeat: false,
|
|
})
|
|
}
|
|
|
|
fn focused_text_input_escape(name: &str) -> Message {
|
|
use iced::advanced::{Layout, Shell, layout, widget};
|
|
let mut field: iced::Element<'_, (), iced::Theme, ()> = text_input("Engineering name", name)
|
|
.id("architecture-name")
|
|
.on_input(|_| ())
|
|
.into();
|
|
let mut tree = widget::Tree::new(field.as_widget());
|
|
let size = iced::Size::new(300., 40.);
|
|
let node =
|
|
field
|
|
.as_widget_mut()
|
|
.layout(&mut tree, &(), &layout::Limits::new(iced::Size::ZERO, size));
|
|
tree.state
|
|
.downcast_mut::<iced::widget::text_input::State<()>>()
|
|
.focus();
|
|
let escape = key_event(iced::keyboard::key::Named::Escape);
|
|
let mut messages = Vec::new();
|
|
let mut shell = Shell::new(&mut messages);
|
|
field.as_widget_mut().update(
|
|
&mut tree,
|
|
&escape,
|
|
Layout::new(&node),
|
|
iced::mouse::Cursor::Unavailable,
|
|
&(),
|
|
&mut iced::advanced::clipboard::Null,
|
|
&mut shell,
|
|
&iced::Rectangle::new(iced::Point::ORIGIN, size),
|
|
);
|
|
assert!(
|
|
shell.is_event_captured(),
|
|
"Iced consumes focused-input Escape"
|
|
);
|
|
assert!(
|
|
!tree
|
|
.state
|
|
.downcast_ref::<iced::widget::text_input::State<()>>()
|
|
.is_focused()
|
|
);
|
|
captured_escape(
|
|
escape,
|
|
iced::event::Status::Captured,
|
|
iced::window::Id::unique(),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_first_focused_input_escape_cancels_create_and_rename() {
|
|
for rename in [false, true] {
|
|
let mut app = setup(Layer::Oa);
|
|
let element = create(&mut app, Kind::Actor, "Accepted dispatcher");
|
|
let model = app.project().clone();
|
|
let source = app.document.bundle().clone();
|
|
let revision = app.document.revision();
|
|
let camera = (app.zoom, app.pan);
|
|
let point = Position { x: 439., y: 328.2 };
|
|
let event = if rename {
|
|
Event::Rename(app.route_context().unwrap(), element, point)
|
|
} else {
|
|
Event::Start(Kind::Actor, Some(point))
|
|
};
|
|
send(&mut app, event);
|
|
let name = " Unaccepted long engineering name 雨 ";
|
|
send(&mut app, Event::Name(name.into()));
|
|
assert!(app.architecture.as_ref().unwrap().inline());
|
|
let message = focused_text_input_escape(name);
|
|
let _ = app.update(message);
|
|
assert!(app.architecture.is_none());
|
|
assert!(inline_editor(&app).is_none());
|
|
assert_eq!(app.status, "Cancelled · Select / move");
|
|
assert_eq!(app.project(), &model);
|
|
assert_eq!(app.document.bundle(), &source);
|
|
assert_eq!(app.document.revision(), revision);
|
|
assert_eq!((app.zoom, app.pan), camera);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn architecture_captured_escape_does_not_cancel_other_controls_or_placement() {
|
|
let mut app = setup(Layer::Oa);
|
|
for event in [
|
|
None,
|
|
Some(Event::Start(Kind::Entity, None)),
|
|
Some(Event::Existing(None)),
|
|
] {
|
|
send(&mut app, Event::Cancel);
|
|
if let Some(event) = event {
|
|
send(&mut app, event);
|
|
}
|
|
let saved = app
|
|
.architecture
|
|
.as_ref()
|
|
.map(|s| serde_json::to_value(&s.saved).unwrap());
|
|
let before = app.document.bundle().clone();
|
|
let status = app.status.clone();
|
|
let epoch = app.canvas_epoch;
|
|
let message = focused_text_input_escape("Other field or existing-element search");
|
|
let _ = app.update(message);
|
|
assert_eq!(
|
|
app.architecture
|
|
.as_ref()
|
|
.map(|s| serde_json::to_value(&s.saved).unwrap()),
|
|
saved
|
|
);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
assert_eq!(app.status, status);
|
|
assert_eq!(app.canvas_epoch, epoch);
|
|
}
|
|
assert!(
|
|
captured_escape(
|
|
key_event(iced::keyboard::key::Named::Escape),
|
|
iced::event::Status::Ignored,
|
|
iced::window::Id::unique(),
|
|
)
|
|
.is_none()
|
|
);
|
|
assert!(
|
|
captured_escape(
|
|
key_event(iced::keyboard::key::Named::Enter),
|
|
iced::event::Status::Captured,
|
|
iced::window::Id::unique(),
|
|
)
|
|
.is_none()
|
|
);
|
|
}
|