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

1268 lines
47 KiB
Rust

//! Real App tree regression tests; no private canvas focus or gesture seeding.
use super::*;
use crate::behavior::canvas_workspace::tests::{Mounted, key};
use iced::advanced::widget::{Operation, operation};
use iced::{Event as Input, Point, Rectangle, Size, Vector, keyboard, mouse};
mod navigation_union;
#[tokio::test]
async fn mounted_state_action_and_internal_rows_delete_relations_and_preserve_reused_work() {
use crate::behavior::state_canvas::visible_row_relation;
for index in 0..4 {
let mut p = Project::empty();
let machine = p
.add_element(Kind::StateMachine, Layer::Sa, "Machine", None)
.unwrap();
let region = p
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
.unwrap();
let state = p
.add_element(Kind::State, Layer::Sa, "Ready", Some(region))
.unwrap();
let other = p
.add_element(Kind::State, Layer::Sa, "Standby", Some(region))
.unwrap();
let work = p
.add_element(Kind::Function, Layer::Sa, "Monitor", None)
.unwrap();
let mut rows = vec![];
for (phase, name) in [
(StateActionKind::Entry, "Start monitoring"),
(StateActionKind::Do, "Continue monitoring"),
(StateActionKind::Exit, "Stop monitoring"),
] {
let id = p.relate(RelationKind::StateAction, name, state, work);
if let RelationBody::StateAction { phase: value, .. } =
&mut p.relations.get_mut(&id).unwrap().body
{
*value = phase;
}
rows.push(id);
}
let retained = p.relate(RelationKind::StateAction, "Other use", other, work);
let internal = p.relate(RelationKind::StateTransition, "Refresh", state, state);
if let RelationBody::StateTransition {
transition_kind, ..
} = &mut p.relations.get_mut(&internal).unwrap().body
{
*transition_kind = TransitionKind::Internal;
}
rows.push(internal);
let target = rows[index];
let plan = crate::behavior::state_edit::create_view(&p, machine).unwrap();
let diagram = plan.diagram;
let p = execute_batch(&p, &[plan.command]).unwrap();
let mut app = App::with_project(p);
app.layer = Layer::Sa;
app.page = Page::Behavior;
app.behavior.active_owner = Some(machine);
app.behavior.diagram.diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let _ = app.update(Message::Select(state));
let mut mounted = Mounted::new(&mut app).await;
app.behavior.diagram.zoom = 1.;
app.behavior.diagram.pan = Vector::new(0., 40.);
app.behavior.diagram.fit = false;
mounted.settle(&mut app).await;
let view = projection::project_view(app.project(), diagram);
assert!(visible_row_relation(app.project(), diagram, &view, target));
assert!(!visible_row_relation(app.project(), diagram, &view, work));
let node = view.nodes.iter().find(|n| n.element == state).unwrap();
let mut collapsed = app.project().clone();
collapsed
.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&node.id)
.unwrap()
.collapsed = true;
assert!(!visible_row_relation(
&collapsed,
diagram,
&projection::project_view(&collapsed, diagram),
target
));
let mut ancestor_collapsed = app.project().clone();
let parent = view.nodes.iter().find(|n| n.element == region).unwrap();
ancestor_collapsed
.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&parent.id)
.unwrap()
.collapsed = true;
assert!(!visible_row_relation(
&ancestor_collapsed,
diagram,
&projection::project_view(&ancestor_collapsed, diagram),
target
));
let unrelated = app
.project()
.diagrams
.values()
.find(|d| d.projection.state_machine().is_none())
.map(|d| d.id);
if let Some(unrelated) = unrelated {
assert!(!visible_row_relation(
app.project(),
unrelated,
&view,
target
));
}
let bounds = mounted
.geometry(&app)
.focused
.into_iter()
.find(|(id, _, _)| {
id.as_ref() == Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner"))
})
.unwrap()
.1;
let row_y = node.position.y + 64. + index as f32 * 28. + 12.;
let at = Point::new(
bounds.x + node.position.x + 45.,
bounds.y + row_y + app.behavior.diagram.pan.y,
);
mounted.click(&mut app, at).await;
assert_eq!(app.selected, target, "actual rendered row identity");
assert!(app.behavior.diagram.placement.is_none());
let before = app.document.snapshot();
delete(&mut mounted, &mut app, false).await;
assert_eq!(
app.delete_target.as_ref().unwrap().snapshot.request.object,
target
);
assert_eq!(app.document.bundle(), &before.bundle);
let task = app.update(Message::CancelDelete);
mounted.perform(&mut app, task).await;
mounted.click(&mut app, at).await;
delete(&mut mounted, &mut app, true).await;
let token = app.delete_target.as_ref().unwrap().snapshot.token;
let task = app.update(Message::ConfirmDelete(token));
mounted.perform(&mut app, task).await;
assert!(!app.project().contains(target));
assert!(
app.project().contains(work)
&& app.project().contains(state)
&& app.project().contains(other)
);
assert!(app.project().contains(retained));
for id in rows.iter().filter(|id| **id != target) {
assert!(app.project().contains(*id));
}
consistent(&app);
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
assert_eq!(app.project(), &before.decoded.project);
assert!(!app.document.can_undo());
}
}
struct Fixture {
app: App,
object: Id,
node: Id,
other: Id,
diagram: Id,
copy: Id,
}
fn fixture(layer: Layer) -> Fixture {
let mut p = Project::empty();
let kind = if layer == Layer::Oa {
Kind::Entity
} else {
Kind::System
};
let object = p.add_element(kind, layer, "Target", None).unwrap();
let other = p
.add_element(
if layer == Layer::Oa {
Kind::Actor
} else {
Kind::SystemActor
},
layer,
"Other",
None,
)
.unwrap();
let diagram = p.ensure_diagram(layer);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
let node = p.place(diagram, object, Position { x: 80., y: 80. });
p.place(diagram, other, Position { x: 500., y: 80. });
let mut second = p.diagrams[&diagram].clone();
second.id = Id::new_v4();
second.name = "Other diagram".into();
second.alias = "other_diagram".into();
second.nodes.clear();
second.routes.clear();
let copy = second.id;
p.diagrams.insert(copy, second);
p.place(copy, object, Position { x: 70., y: 70. });
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = layer;
app.active_diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
Fixture {
app,
object,
node,
other,
diagram,
copy,
}
}
async fn mount(layer: Layer) -> (Fixture, Mounted) {
let mut f = fixture(layer);
let mut mounted = Mounted::new(&mut f.app).await;
f.app.zoom = 1.;
f.app.pan = Vector::ZERO;
f.app.fit_to_view = false;
mounted.settle(&mut f.app).await;
select(&mut mounted, &mut f).await;
(f, mounted)
}
async fn select(mounted: &mut Mounted, f: &mut Fixture) {
let bounds = mounted
.geometry(&f.app)
.bounds(crate::allocation_input::CANVAS_ID);
let node = projection::project_view(f.app.project(), f.diagram)
.nodes
.into_iter()
.find(|node| node.id == f.node)
.unwrap();
let point = Point::new(
bounds.x + (node.position.x + 40.) * f.app.zoom + f.app.pan.x,
bounds.y + (node.position.y + 35.) * f.app.zoom + f.app.pan.y,
);
mounted.click(&mut f.app, point).await;
assert_eq!(f.app.selected, f.object);
assert_eq!(f.app.selected_node, Some(f.node));
}
fn modified(named: keyboard::key::Named, modifiers: keyboard::Modifiers) -> Input {
let mut input = key(named);
if let Input::Keyboard(keyboard::Event::KeyPressed {
modifiers: value, ..
}) = &mut input
{
*value = modifiers;
}
input
}
async fn delete(mounted: &mut Mounted, app: &mut App, shift: bool) -> Vec<Message> {
mounted
.batch(
app,
&[modified(
keyboard::key::Named::Delete,
if shift {
keyboard::Modifiers::SHIFT
} else {
keyboard::Modifiers::empty()
},
)],
mouse::Cursor::Unavailable,
)
.await
}
fn consistent(app: &App) {
assert_eq!(
source_project::compile_bundle(app.document.bundle(), LIMITS)
.unwrap()
.decoded
.project,
*app.project()
);
assert_eq!(
source_workspace::reachable(&app.source_workspace.bundle()).unwrap(),
*app.document.bundle()
);
}
async fn save_with_busy_view(mounted: &mut Mounted, app: &mut App, path: &std::path::Path) {
let before = app.document.bundle().clone();
let task = app.save_to(path.to_owned());
assert!(app.busy);
// Render and exercise the real busy tree before completing the real save.
let messages = mounted
.batch(
app,
&[
key(keyboard::key::Named::F2),
modified(keyboard::key::Named::Delete, keyboard::Modifiers::SHIFT),
],
mouse::Cursor::Unavailable,
)
.await;
assert!(messages.iter().all(|message| !matches!(
message,
Message::DiagramRemove(_)
| Message::StateDiagram(crate::behavior::state_edit::Event::Remove(_))
| Message::Architecture(crate::app::architecture_authoring::Event::Rename(..))
)));
assert_eq!(app.document.bundle(), &before);
mounted.perform(app, task).await;
assert!(!app.busy);
assert!(path.is_file());
assert!(!app.document.dirty(), "{}", app.status);
assert_eq!(app.document.bundle(), &before);
}
#[tokio::test]
async fn mounted_oa_sa_delete_hides_exact_placement_and_round_trips_source_and_model() {
for layer in [Layer::Oa, Layer::Sa] {
let (mut f, mut mounted) = mount(layer).await;
let before = f.app.document.snapshot();
let messages = delete(&mut mounted, &mut f.app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_)))
);
assert!(f.app.project().contains(f.object));
assert!(
!f.app.project().diagrams[&f.diagram]
.nodes
.contains_key(&f.node)
);
assert!(
f.app.project().diagrams[&f.copy]
.nodes
.values()
.any(|node| node.element == f.object)
);
assert!(!f.app.delete_pending);
consistent(&f.app);
let after = f.app.document.snapshot();
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.document.can_undo(), "one removal, one undo unit");
let task = f.app.update(Message::Redo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.project(), &after.decoded.project);
assert_eq!(f.app.document.bundle(), &after.bundle);
consistent(&f.app);
// History changes the revision, while this same canvas still owns the
// keyboard. Repeated hide and then model deletion need no extra click.
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
delete(&mut mounted, &mut f.app, false).await;
assert_eq!(f.app.document.bundle(), &after.bundle);
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
delete(&mut mounted, &mut f.app, true).await;
assert_eq!(
f.app
.delete_target
.as_ref()
.unwrap()
.snapshot
.request
.object,
f.object
);
assert_eq!(f.app.document.bundle(), &before.bundle);
}
}
#[tokio::test]
async fn mounted_shift_delete_captures_target_cancel_escape_and_confirmation_replay() {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
delete(&mut mounted, &mut f.app, true).await;
let target = f.app.delete_target.as_ref().unwrap();
assert_eq!(target.snapshot.request.object, f.object);
let stale_token = target.snapshot.token;
let cancel = mounted.geometry(&f.app).bounds("delete-cancel").center();
mounted.click(&mut f.app, cancel).await;
let task = f.app.update(Message::ConfirmDelete(stale_token));
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
select(&mut mounted, &mut f).await;
delete(&mut mounted, &mut f.app, true).await;
let token = f.app.delete_target.as_ref().unwrap().snapshot.token;
let task = f.app.update(Message::ConfirmDelete(stale_token));
mounted.perform(&mut f.app, task).await;
assert!(
f.app.delete_pending,
"old dialog must not accept a new confirmation"
);
assert_eq!(f.app.delete_target.as_ref().unwrap().snapshot.token, token);
let confirm = mounted.geometry(&f.app).bounds("delete-confirm").center();
mounted.click(&mut f.app, confirm).await;
assert!(!f.app.project().contains(f.object));
assert!(f.app.project().contains(f.other));
consistent(&f.app);
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
select(&mut mounted, &mut f).await;
delete(&mut mounted, &mut f.app, true).await;
let task = f.app.update(Message::Escape);
mounted.perform(&mut f.app, task).await;
assert!(!f.app.delete_pending);
assert!(f.app.delete_target.is_none());
assert_eq!(f.app.document.bundle(), &before.bundle);
}
struct FocusField(Rectangle);
#[tokio::test]
async fn mounted_placement_properties_selection_supports_hide_and_model_delete() {
for layer in [Layer::Oa, Layer::Sa] {
for model in [false, true] {
let (mut f, mut mounted) = mount(layer).await;
let before = f.app.document.snapshot();
let mut request = crate::model_menu::Request::object(f.object, Point::ORIGIN);
request.placement = Some(f.node);
request.canvas = f.app.route_context();
let task = f
.app
.update(Message::Menu(crate::model_menu::Event::Open(request)));
mounted.perform(&mut f.app, task).await;
let action = crate::model_menu::popup(&f.app)
.unwrap()
.entries
.into_iter()
.find_map(|entry| match entry {
crate::context_menu::Entry::Action { label, message, .. }
if label == "Placement properties" =>
{
message
}
_ => None,
})
.unwrap();
let task = f.app.update(action);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.selected, f.node);
assert_eq!(f.app.selected_node, Some(f.node));
let bounds = mounted
.geometry(&f.app)
.bounds(crate::allocation_input::CANVAS_ID);
mounted
.click(&mut f.app, Point::new(bounds.x + 20., bounds.y + 20.))
.await;
assert_eq!(
f.app.selected, f.node,
"blank canvas preserves placement selection"
);
delete(&mut mounted, &mut f.app, model).await;
if model {
let target = f
.app
.delete_target
.as_ref()
.expect("model confirmation opens");
assert_eq!(target.snapshot.request.object, f.object);
assert_eq!(target.selection, f.node);
let token = target.snapshot.token;
let task = f.app.update(Message::ConfirmDelete(token));
mounted.perform(&mut f.app, task).await;
assert!(!f.app.project().contains(f.object));
assert!(
!f.app.project().diagrams[&f.copy]
.nodes
.values()
.any(|n| n.element == f.object)
);
} else {
assert!(f.app.project().contains(f.object));
assert!(!f.app.project().contains(f.node));
assert!(
f.app.project().diagrams[&f.copy]
.nodes
.values()
.any(|n| n.element == f.object)
);
}
consistent(&f.app);
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert_eq!(f.app.project(), &before.decoded.project);
assert!(!f.app.document.can_undo());
}
}
}
impl Operation for FocusField {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
fn focusable(
&mut self,
_: Option<&iced::advanced::widget::Id>,
bounds: Rectangle,
state: &mut dyn operation::Focusable,
) {
if bounds == self.0 {
state.focus();
} else {
state.unfocus();
}
}
}
fn name_field(mounted: &mut Mounted, app: &App) -> Rectangle {
mounted
.geometry(app)
.text_inputs
.into_iter()
.filter(|bounds| bounds.x > app.window_size.width - 320.)
.min_by(|a, b| a.y.total_cmp(&b.y))
.unwrap()
}
#[tokio::test]
async fn mounted_clean_text_programmatic_focus_and_tab_remove_canvas_keyboard_ownership() {
for programmatic in [true, false] {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
let field = name_field(&mut mounted, &f.app);
if programmatic {
mounted.operate(&f.app, &mut FocusField(field));
} else {
let messages = mounted
.batch(
&mut f.app,
&[
key(keyboard::key::Named::Tab),
key(keyboard::key::Named::Delete),
],
mouse::Cursor::Unavailable,
)
.await;
assert!(
!messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_))),
"Tab retires canvas admission before its queued focus operation runs"
);
}
let geometry = mounted.geometry(&f.app);
assert!(
geometry
.focused
.iter()
.any(|(_, bounds, focused)| *bounds == field && *focused),
"clean Inspector receives real focus"
);
assert!(!f.app.draft_dirty());
assert!(
delete(&mut mounted, &mut f.app, true)
.await
.iter()
.all(|message| !matches!(message, Message::DiagramRemove(_)))
);
assert!(!f.app.delete_pending);
mounted
.batch(
&mut f.app,
&[key(keyboard::key::Named::Home)],
mouse::Cursor::Unavailable,
)
.await;
let messages = delete(&mut mounted, &mut f.app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::Field(path, _) if path == "/name"))
);
assert_eq!(f.app.draft.as_ref().unwrap().inputs["/name"].raw, "arget");
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.node));
assert!(!f.app.delete_pending);
}
}
#[tokio::test]
async fn mounted_revision_and_history_preserve_the_actual_canvas_or_inspector_focus_owner() {
for inspector in [false, true] {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join(&f.app.document.bundle().entry);
let field = name_field(&mut mounted, &f.app);
if inspector {
mounted.click(&mut f.app, field.center()).await;
}
let before = f.app.document.snapshot();
let task = f.app.update(Message::Command(Command::Rename(
f.other,
"Changed other element".into(),
)));
mounted.perform(&mut f.app, task).await;
for message in [Message::Undo, Message::Redo, Message::Undo] {
let task = f.app.update(message);
mounted.perform(&mut f.app, task).await;
save_with_busy_view(&mut mounted, &mut f.app, &path).await;
let geometry = mounted.geometry(&f.app);
assert_eq!(
geometry.focused.iter().any(|(id, _, focused)| {
*focused
&& id.as_ref()
== Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner"))
}),
!inspector,
"history cannot grant the canvas focus held by an Inspector field"
);
assert!(geometry.focused.iter().any(|(id, bounds, focused)| {
*focused
&& if inspector {
*bounds == field
} else {
id.as_ref()
== Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner"))
}
}));
}
assert_eq!(f.app.document.bundle(), &before.bundle);
let messages = mounted
.batch(
&mut f.app,
&[key(keyboard::key::Named::F2)],
mouse::Cursor::Unavailable,
)
.await;
assert_eq!(
messages.iter().any(|message| matches!(
message,
Message::Architecture(crate::app::architecture_authoring::Event::Rename(..))
)),
!inspector,
"F2 follows the actual focus owner after edits and history"
);
// Both the existing Inspector and the newly opened inline rename own
// their text. A second F2 or Delete must not reach the diagram.
let messages = mounted
.batch(
&mut f.app,
&[
key(keyboard::key::Named::F2),
key(keyboard::key::Named::Delete),
],
mouse::Cursor::Unavailable,
)
.await;
assert!(messages.iter().all(|message| !matches!(
message,
Message::DiagramRemove(_)
| Message::Architecture(crate::app::architecture_authoring::Event::Rename(..))
)));
assert!(!f.app.delete_pending);
assert_eq!(f.app.document.bundle(), &before.bundle);
}
}
#[tokio::test]
async fn mounted_passive_save_never_grants_canvas_focus_or_restores_window_lost_focus() {
let mut f = fixture(Layer::Oa);
let _ = f.app.update(Message::Select(f.object));
let mut mounted = Mounted::new(&mut f.app).await;
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join(&f.app.document.bundle().entry);
let task = f.app.save_to(path.clone());
assert!(f.app.busy);
let focus =
iced::widget::operation::focus(iced::advanced::widget::Id::from("diagram-keyboard-owner"));
mounted.perform(&mut f.app, focus).await;
let canvas = mounted
.geometry(&f.app)
.bounds(crate::allocation_input::CANVAS_ID);
mounted.click(&mut f.app, canvas.center()).await;
mounted.perform(&mut f.app, task).await;
assert!(!f.app.busy);
assert!(delete(&mut mounted, &mut f.app, false).await.is_empty());
assert!(f.app.project().contains(f.node));
select(&mut mounted, &mut f).await;
let task = f.app.save_to(path);
assert!(f.app.busy);
mounted
.batch(
&mut f.app,
&[Input::Window(iced::window::Event::Unfocused)],
mouse::Cursor::Unavailable,
)
.await;
mounted.perform(&mut f.app, task).await;
assert!(!f.app.busy);
assert!(delete(&mut mounted, &mut f.app, false).await.is_empty());
assert!(f.app.project().contains(f.node));
}
#[tokio::test]
async fn mounted_inspector_can_take_focus_during_passive_save() {
for programmatic in [false, true] {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let field = name_field(&mut mounted, &f.app);
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join(&f.app.document.bundle().entry);
let task = f.app.save_to(path);
assert!(f.app.busy);
if programmatic {
mounted.operate(&f.app, &mut FocusField(field));
} else {
mounted.click(&mut f.app, field.center()).await;
}
mounted.perform(&mut f.app, task).await;
let geometry = mounted.geometry(&f.app);
assert!(
geometry
.focused
.iter()
.any(|(_, bounds, focused)| { *bounds == field && *focused })
);
assert!(!geometry.focused.iter().any(|(id, _, focused)| {
*focused
&& id.as_ref() == Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner"))
}));
let before = f.app.document.bundle().clone();
let messages = mounted
.batch(
&mut f.app,
&[
key(keyboard::key::Named::F2),
key(keyboard::key::Named::Delete),
],
mouse::Cursor::Unavailable,
)
.await;
assert!(messages.iter().all(|message| !matches!(
message,
Message::DiagramRemove(_)
| Message::Architecture(crate::app::architecture_authoring::Event::Rename(..))
)));
assert!(!f.app.delete_pending);
assert_eq!(f.app.document.bundle(), &before);
}
}
#[tokio::test]
async fn mounted_passive_save_retires_held_canvas_drag_but_allows_a_fresh_drag() {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let bounds = mounted
.geometry(&f.app)
.bounds(crate::allocation_input::CANVAS_ID);
let start = bounds.position() + Vector::new(120., 115.);
let moved = start + Vector::new(35., 20.);
let before = f.app.document.snapshot();
mounted
.batch(
&mut f.app,
&[
Input::Mouse(mouse::Event::CursorMoved { position: start }),
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
Input::Mouse(mouse::Event::CursorMoved { position: moved }),
],
mouse::Cursor::Available(moved),
)
.await;
assert_eq!(f.app.document.bundle(), &before.bundle);
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join(&f.app.document.bundle().entry);
let task = f.app.save_to(path);
assert!(f.app.busy);
mounted
.batch(
&mut f.app,
&[Input::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Left,
))],
mouse::Cursor::Available(moved),
)
.await;
mounted.perform(&mut f.app, task).await;
let end = moved + Vector::new(50., 30.);
mounted
.batch(
&mut f.app,
&[
Input::Mouse(mouse::Event::CursorMoved { position: end }),
Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
],
mouse::Cursor::Available(end),
)
.await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.document.can_undo());
mounted
.batch(
&mut f.app,
&[
Input::Mouse(mouse::Event::CursorMoved { position: start }),
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
Input::Mouse(mouse::Event::CursorMoved { position: moved }),
Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
],
mouse::Cursor::Available(moved),
)
.await;
assert_ne!(f.app.document.bundle(), &before.bundle);
assert!(f.app.document.can_undo());
consistent(&f.app);
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
}
#[tokio::test]
async fn mounted_removal_intents_reject_stale_selection_revision_owner_and_document() {
for boundary in 0..5 {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let messages = mounted.input(
&f.app,
&[key(keyboard::key::Named::Delete)],
mouse::Cursor::Unavailable,
);
let intent = messages
.into_iter()
.find(|message| matches!(message, Message::DiagramRemove(_)))
.unwrap();
match boundary {
0 => {
let _ = f.app.update(Message::Select(f.other));
}
1 => {
f.app.canvas_epoch = f.app.canvas_epoch.wrapping_add(1);
}
2 => {
f.app.active_diagram = Some(f.copy);
}
3 => {
f.app.canvas_document = Id::new_v4();
}
_ => {
let _ = f
.app
.update(Message::Command(Command::Rename(f.other, "Renamed".into())));
}
}
let before = f.app.document.snapshot();
let task = f.app.update(intent);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.node));
assert!(!f.app.delete_pending);
}
}
#[tokio::test]
async fn mounted_window_modal_dirty_and_modifier_boundaries_retire_delete_focus() {
for boundary in 0..5 {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
match boundary {
0 => {
mounted
.batch(
&mut f.app,
&[Input::Window(iced::window::Event::Unfocused)],
mouse::Cursor::Unavailable,
)
.await;
}
1 => {
let _ = f.app.update(Message::Menu(crate::model_menu::Event::Open(
crate::model_menu::Request::object(f.object, Point::ORIGIN),
)));
mounted.settle(&mut f.app).await;
let _ = f.app.update(Message::Menu(crate::model_menu::Event::Close));
}
2 => {
let _ = f.app.update(Message::Field("/name".into(), "Dirty".into()));
}
3 => {
let _ = f.app.update(Message::Delete);
}
_ => {
f.app.canvas_epoch = f.app.canvas_epoch.wrapping_add(1);
}
}
delete(&mut mounted, &mut f.app, false).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.node));
}
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
mounted
.batch(
&mut f.app,
&[modified(
keyboard::key::Named::Delete,
keyboard::Modifiers::CTRL,
)],
mouse::Cursor::Unavailable,
)
.await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.delete_pending);
// Refusal is temporary: a fresh live canvas click owns Delete again.
select(&mut mounted, &mut f).await;
delete(&mut mounted, &mut f.app, false).await;
assert!(!f.app.project().contains(f.node));
}
#[tokio::test]
async fn mounted_bend_delete_has_priority_and_semantic_edge_delete_is_confirmed() {
let mut p = Project::empty();
let a = p.add(Kind::Activity, "Send", 0., 0.);
let b = p.add(Kind::Activity, "Receive", 650., 0.);
let edge = p.relate(RelationKind::Interaction, "Report", a, b);
let diagram = p.ensure_diagram(Layer::Oa);
let p = execute_batch(
&p,
&[Command::SetEdgeRoute {
diagram,
route: EdgeRoute {
edge,
source_attachment: None,
target_attachment: None,
waypoints: vec![Position { x: 400., y: -100. }],
label: None,
},
}],
)
.unwrap();
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.active_diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let _ = app.update(Message::Select(edge));
let mut mounted = Mounted::new(&mut app).await;
app.zoom = 1.;
app.pan = Vector::new(100., 250.);
app.fit_to_view = false;
mounted.settle(&mut app).await;
let bounds = mounted
.geometry(&app)
.bounds(crate::allocation_input::CANVAS_ID);
let pin = Point::new(bounds.x + 500., bounds.y + 150.);
mounted.click(&mut app, pin).await;
assert_eq!(
app.selected_bend,
Some((edge, 0)),
"real pointer selects route pin"
);
let before = app.document.snapshot();
let messages = delete(&mut mounted, &mut app, false).await;
assert!(messages.iter().any(|message| matches!(
message,
Message::Route(crate::route_edit::Event::RemoveBend)
)));
assert!(
!messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_)))
);
assert!(app.project().contains(edge) && app.project().contains(a) && app.project().contains(b));
assert!(
crate::route_edit::stored_route(&app.project().diagrams[&diagram], edge)
.waypoints
.is_empty()
);
assert!(!app.delete_pending);
consistent(&app);
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
// Select a real edge segment away from its handle, then Delete opens the
// same semantic confirmation instead of inventing an edge hide operation.
let view = projection::project_view(app.project(), diagram);
let routed = routing::route_diagram(app.project(), &app.project().diagrams[&diagram], &view);
let route = &routed.edges[&edge];
let segment = route
.points
.windows(2)
.max_by(|a, b| {
let len = |s: &[Position]| (s[1].x - s[0].x).abs() + (s[1].y - s[0].y).abs();
len(a).total_cmp(&len(b))
})
.unwrap();
let at = Point::new(
bounds.x + (segment[0].x + segment[1].x) / 2. + app.pan.x,
bounds.y + (segment[0].y + segment[1].y) / 2. + app.pan.y,
);
mounted.click(&mut app, at).await;
assert_eq!(app.selected, edge);
assert!(app.selected_bend.is_none());
delete(&mut mounted, &mut app, false).await;
assert_eq!(
app.delete_target.as_ref().unwrap().snapshot.request.object,
edge
);
assert_eq!(app.document.bundle(), &before.bundle);
}
#[tokio::test]
async fn mounted_state_delete_uses_state_placement_and_preserves_architecture_placement() {
let mut p = Project::empty();
let machine = p
.add_element(Kind::StateMachine, Layer::Sa, "Machine", None)
.unwrap();
let region = p
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
.unwrap();
let state = p
.add_element(Kind::State, Layer::Sa, "Ready", Some(region))
.unwrap();
let architecture = p.ensure_diagram(Layer::Sa);
let architecture_node = p.place(architecture, state, Position { x: 50., y: 50. });
let plan = crate::behavior::state_edit::create_view(&p, machine).unwrap();
let diagram = plan.diagram;
let p = execute_batch(&p, &[plan.command]).unwrap();
let node = p.diagrams[&diagram]
.nodes
.values()
.find(|node| node.element == state)
.unwrap()
.id;
let mut app = App::with_project(p);
app.layer = Layer::Sa;
app.page = Page::Behavior;
app.active_diagram = Some(architecture);
app.behavior.active_owner = Some(machine);
app.behavior.diagram.diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let _ = app.update(Message::Select(state));
let mut mounted = Mounted::new(&mut app).await;
let bounds = mounted
.geometry(&app)
.focused
.into_iter()
.find(|(id, _, _)| {
id.as_ref() == Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner"))
})
.unwrap()
.1;
let projected = projection::project_view(app.project(), diagram)
.nodes
.into_iter()
.find(|n| n.id == node)
.unwrap();
let at = Point::new(
bounds.x
+ (projected.position.x + 35.) * app.behavior.diagram.zoom
+ app.behavior.diagram.pan.x,
bounds.y
+ (projected.position.y + 35.) * app.behavior.diagram.zoom
+ app.behavior.diagram.pan.y,
);
mounted.click(&mut app, at).await;
assert_eq!(app.selected, state);
assert_eq!(app.behavior.diagram.placement, Some(node));
let before = app.document.snapshot();
assert!(crate::behavior::state_edit::unplaced(app.project(), diagram).is_empty());
let owner = app.state_context().unwrap();
let focused = |mounted: &mut Mounted, app: &App| {
mounted
.geometry(app)
.focused
.iter()
.any(|(id, _, focused)| {
*focused
&& id.as_ref()
== Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner"))
})
};
assert!(focused(&mut mounted, &app));
let messages = delete(&mut mounted, &mut app, false).await;
assert!(messages.iter().any(|message| matches!(
message,
Message::StateDiagram(crate::behavior::state_edit::Event::Remove(_))
)));
assert!(!app.project().contains(node));
assert!(app.project().contains(architecture_node) && app.project().contains(state));
assert_eq!(
crate::behavior::state_edit::unplaced(app.project(), diagram),
vec![state]
);
mounted.settle(&mut app).await;
let hidden = app.state_context().unwrap();
assert_eq!(
(hidden.document, hidden.diagram, hidden.epoch),
(owner.document, owner.diagram, owner.epoch),
"hiding one placement does not change the logical canvas owner"
);
assert!(
focused(&mut mounted, &app),
"showing the unplaced-element picker must retain the actual canvas focus tree"
);
consistent(&app);
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join(&app.document.bundle().entry);
save_with_busy_view(&mut mounted, &mut app, &path).await;
assert!(focused(&mut mounted, &app), "Save retains the same owner");
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
assert!(crate::behavior::state_edit::unplaced(app.project(), diagram).is_empty());
mounted.settle(&mut app).await;
assert!(
focused(&mut mounted, &app),
"Undo removes the picker without replacing the actual canvas focus tree"
);
delete(&mut mounted, &mut app, true).await;
assert_eq!(
app.delete_target.as_ref().unwrap().snapshot.request.object,
state
);
assert_eq!(app.document.bundle(), &before.bundle);
}
#[tokio::test]
async fn captured_confirmation_rejects_selection_document_revision_and_placement_changes() {
for boundary in 0..5 {
let (mut f, mut mounted) = mount(Layer::Oa).await;
delete(&mut mounted, &mut f.app, true).await;
let token = f.app.delete_target.as_ref().unwrap().snapshot.token;
// Model asynchronous adoption / retained stale UI using a changed App
// state while preserving the old confirmation payload deliberately.
match boundary {
0 => {
f.app.selected = f.other;
}
1 => {
f.app.canvas_document = Id::new_v4();
}
2 => {
f.app.canvas_epoch = f.app.canvas_epoch.wrapping_add(1);
}
3 => {
f.app.selected_node = None;
}
_ => {
assert!(
f.app
.document
.execute(Command::Rename(f.other, "New revision".into()))
.is_ok()
);
}
}
let before = f.app.document.snapshot();
let task = f.app.update(Message::ConfirmDelete(token));
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.object));
assert!(!f.app.delete_pending);
}
}
#[tokio::test]
async fn mounted_programmatic_canvas_entry_needs_no_mouse_focus_boolean() {
let mut f = fixture(Layer::Oa);
let _ = f.app.update(Message::Select(f.object));
let mut mounted = Mounted::new(&mut f.app).await;
let task =
iced::widget::operation::focus(iced::advanced::widget::Id::from("diagram-keyboard-owner"));
mounted.perform(&mut f.app, task).await;
let messages = delete(&mut mounted, &mut f.app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_)))
);
assert!(!f.app.project().contains(f.node));
assert!(f.app.project().contains(f.object));
}
#[tokio::test]
async fn mounted_allocation_work_click_then_delete_without_redraw_removes_placement_once() {
for layer in [Layer::Oa, Layer::Sa] {
let mut p = Project::empty();
let performer = p
.add_element(
if layer == Layer::Oa {
Kind::Entity
} else {
Kind::System
},
layer,
"Performer",
None,
)
.unwrap();
let work = p
.add_element(
if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
},
layer,
"Work",
None,
)
.unwrap();
let diagram = p.ensure_diagram(layer);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
p.place(diagram, performer, Position { x: 100., y: 100. });
let node = p.place(diagram, work, Position { x: 100., y: 500. });
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = layer;
app.active_diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let mut mounted = Mounted::new(&mut app).await;
app.zoom = 1.;
app.pan = Vector::ZERO;
app.fit_to_view = false;
mounted.settle(&mut app).await;
let bounds = mounted
.geometry(&app)
.bounds(crate::allocation_input::CANVAS_ID);
let point = Point::new(bounds.x + 140., bounds.y + 535.);
let messages = mounted.click(&mut app, point).await;
assert!(messages.iter().any(|message| matches!(
message,
Message::WorkDrag(crate::app::allocation_drag::Event::Press(_))
)));
assert_eq!(app.selected, work);
assert_eq!(app.selected_node, Some(node));
let before = app.document.snapshot();
// Deliberately do not settle/redraw between completed click and key.
let messages = delete(&mut mounted, &mut app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_))),
"retired allocation press cannot consume first Delete"
);
assert!(!app.project().contains(node));
assert!(app.project().contains(work) && app.project().contains(performer));
consistent(&app);
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join(&app.document.bundle().entry);
save_with_busy_view(&mut mounted, &mut app, &path).await;
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
save_with_busy_view(&mut mounted, &mut app, &path).await;
let task = app.update(Message::Redo);
mounted.perform(&mut app, task).await;
assert!(!app.project().contains(node));
save_with_busy_view(&mut mounted, &mut app, &path).await;
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
// Native regression: work click -> hide -> Undo/Redo/Undo -> Shift+Delete.
// No pointer event or explicit focus operation occurs after the click.
delete(&mut mounted, &mut app, true).await;
assert_eq!(
app.delete_target.as_ref().unwrap().snapshot.request.object,
work
);
assert_eq!(app.document.bundle(), &before.bundle);
}
}