//! Delete through the actual App widget tree, ordinary focus, and source history. use super::*; use crate::behavior::{ canvas_workspace::tests::{Mounted, focused, key}, scenario, topology, }; use iced::{Event as Input, Point, Size, Vector, keyboard, mouse}; fn app(p: Project, owner: Id) -> App { let mut app = App::with_project(p); let _ = app.update(Message::Select(owner)); let _ = app.update(Message::Page(Page::Behavior)); app.window_size = Size::new(1900., 1200.); app } fn point(mounted: &mut Mounted, app: &App, id: Id) -> Point { if let Some(context) = app.topology_context() { let bounds = mounted.geometry(app).bounds("process-viewport"); let local = app .behavior .topology .removal_test_point(app, context.owner, id, None); bounds.position() + Vector::new(local.x, local.y) } else { let context = app.scenario_context().unwrap(); let bounds = mounted.geometry(app).bounds("scenario-viewport"); let local = scenario::removal_test_point(app, context.scenario, id); bounds.position() + Vector::new(local.x, local.y) } } async fn select(mounted: &mut Mounted, app: &mut App, id: Id) { let target = point(mounted, app, id); mounted.click(app, target).await; assert_eq!(app.selected, id, "real pointer selects intended occurrence"); } fn remove_key(shift: bool) -> Input { let mut event = key(keyboard::key::Named::Delete); if shift && let Input::Keyboard(keyboard::Event::KeyPressed { modifiers, .. }) = &mut event { *modifiers = keyboard::Modifiers::SHIFT; } event } fn removal(message: &Message) -> bool { matches!( message, Message::TopologyCanvas(topology::CanvasEvent::Remove(..)) | Message::ScenarioCanvas(scenario::CanvasEvent::Remove(..)) ) } async fn delete(mounted: &mut Mounted, app: &mut App, shift: bool) -> u64 { let messages = mounted .batch(app, &[remove_key(shift)], mouse::Cursor::Unavailable) .await; assert!( messages.iter().any(removal), "focused viewport emits removal: {messages:?}" ); let target = app.delete_target.as_ref().expect("captured confirmation"); assert_eq!(target.snapshot.request.object, app.selected); assert_eq!(target.placement, None); assert_eq!(target.snapshot.request.placement, None); target.snapshot.token } async fn send(mounted: &mut Mounted, app: &mut App, message: Message) { let task = app.update(message); mounted.perform(app, task).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() ); } #[tokio::test] async fn mounted_process_delete_occurrence_or_link_preserves_reusable_work_and_source_history() { for layer in [Layer::Oa, Layer::Sa] { for link in [false, true] { let f = topology::direct_tests::fixture(layer); let target = if link { f.link } else { f.nodes[0] }; let mut app = app(f.p.clone(), f.owner); let mut mounted = Mounted::new(&mut app).await; let original = app.document.bundle().clone(); select(&mut mounted, &mut app, target).await; // No extra redraw between release and first Delete. let token = delete(&mut mounted, &mut app, link).await; assert_eq!( app.delete_target.as_ref().unwrap().behavior_owner, Some(f.owner) ); assert_eq!(app.document.bundle(), &original); send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await; assert!(!app.project().contains(target)); assert!(f.work.iter().all(|id| app.project().contains(*id))); assert!(app.project().contains(f.exchange)); assert!(app.project().contains(f.nodes[1])); assert_eq!(app.project().contains(f.nodes[0]), link); assert!(!app.project().contains(f.link)); consistent(&app); let removed = app.document.bundle().clone(); send(&mut mounted, &mut app, Message::Undo).await; assert_eq!(app.document.bundle(), &original); consistent(&app); send(&mut mounted, &mut app, Message::Redo).await; assert_eq!(app.document.bundle(), &removed); consistent(&app); } } } #[tokio::test] async fn mounted_scenario_delete_message_or_participant_preserves_reusable_targets() { for participant in [false, true] { let f = scenario::direct_tests::fixture(); let target = if participant { f.sender } else { f.call }; let mut app = app(f.project.clone(), f.owner); let mut mounted = Mounted::new(&mut app).await; let original = app.document.bundle().clone(); select(&mut mounted, &mut app, target).await; let token = delete(&mut mounted, &mut app, participant).await; send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await; assert!(!app.project().contains(target)); assert!(app.project().contains(f.exchange)); assert!(app.project().contains(f.receiver)); assert!(app.project().contains(f.reply)); for element in f .project .elements .values() .filter(|element| element.parent != Some(f.owner)) { assert!( app.project().contains(element.id), "referenced definitions survive" ); } if participant { assert!(app.project().contains(f.call)); for element in app.project().elements.values() { if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { sender, receiver, .. }, .. }) = element.shared() { assert_ne!(*sender, Some(target)); assert_ne!(*receiver, Some(target)); } } } consistent(&app); send(&mut mounted, &mut app, Message::Undo).await; assert_eq!(app.document.bundle(), &original); consistent(&app); } } #[tokio::test] async fn mounted_scenario_use_delete_keeps_definition_and_second_use_and_rejects_owner_replay() { let (p, owners, uses) = scenario::direct_tests::navigation_fixture(); let mut app = app(p, owners[0]); let mut mounted = Mounted::new(&mut app).await; let original = app.document.bundle().clone(); select(&mut mounted, &mut app, uses[0]).await; let stale = delete(&mut mounted, &mut app, false).await; send(&mut mounted, &mut app, Message::CancelDelete).await; send(&mut mounted, &mut app, Message::ConfirmDelete(stale)).await; assert_eq!(app.document.bundle(), &original); select(&mut mounted, &mut app, uses[0]).await; let token = delete(&mut mounted, &mut app, true).await; send(&mut mounted, &mut app, Message::ConfirmDelete(stale)).await; assert!(app.delete_pending, "old token does not cancel new dialog"); // Keep selection and epoch deliberately unchanged: owner is an independent witness. let captured = app.scenario_context().unwrap(); assert_eq!(captured.scenario, owners[0]); assert_eq!( app.delete_target.as_ref().unwrap().behavior_owner, Some(owners[0]) ); app.behavior.active_owner = Some(owners[1]); app.reconcile_scenario_workspace(); let other = app.scenario_context().unwrap(); assert_eq!(other.scenario, owners[1]); assert_eq!( other.representation, crate::behavior::scenario_layout::Representation::Derived { owner: owners[1] } ); assert_eq!(other.epoch, captured.epoch); assert_eq!(app.selected, uses[0]); assert!( app.delete_target.as_ref().unwrap().snapshot.valid(&app), "the independent owner guard must reject an otherwise current token" ); send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await; assert_eq!(app.document.bundle(), &original); assert!(!app.delete_pending && app.delete_target.is_none()); assert!(!app.document.can_undo() && !app.document.can_redo()); app.behavior.active_owner = Some(owners[0]); // Restore both owner and qualified presentation before mounting pointer input. // Passive reports from the previous owner cannot repair an inconsistent fixture. app.reconcile_scenario_workspace(); assert_eq!(app.scenario_context().unwrap().scenario, owners[0]); assert_eq!(app.selected, uses[0]); mounted.settle(&mut app).await; select(&mut mounted, &mut app, uses[0]).await; let token = delete(&mut mounted, &mut app, false).await; send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await; assert!(!app.project().contains(uses[0])); assert!(app.project().contains(uses[1])); assert!(owners.iter().all(|id| app.project().contains(*id))); consistent(&app); send(&mut mounted, &mut app, Message::Undo).await; assert_eq!(app.document.bundle(), &original); } #[tokio::test] async fn mounted_behavior_tab_and_f6_retire_same_batch_delete_and_window_focus() { for process in [false, true] { let (mut app, target) = if process { let f = topology::direct_tests::fixture(Layer::Oa); (app(f.p, f.owner), f.nodes[0]) } else { let f = scenario::direct_tests::fixture(); (app(f.project, f.owner), f.call) }; let mut mounted = Mounted::new(&mut app).await; let original = app.document.bundle().clone(); for boundary in [ keyboard::key::Named::Tab, keyboard::key::Named::F6, keyboard::key::Named::Escape, ] { select(&mut mounted, &mut app, target).await; let messages = mounted .batch( &mut app, &[key(boundary), remove_key(false)], mouse::Cursor::Unavailable, ) .await; assert!( !messages.iter().any(removal), "{boundary:?} retires keyboard admission within the batch" ); assert!(!app.delete_pending); assert_eq!(app.document.bundle(), &original); assert!(!focused(&mut mounted, &app, "viewport")); if boundary == keyboard::key::Named::Tab { assert_eq!( mounted .geometry(&app) .focused .iter() .filter(|(_, _, value)| *value) .count(), 1, "Tab completes traversal to one real control" ); let mut backwards = key(keyboard::key::Named::Tab); if let Input::Keyboard(keyboard::Event::KeyPressed { modifiers, .. }) = &mut backwards { *modifiers = keyboard::Modifiers::SHIFT; } mounted .batch(&mut app, &[backwards], mouse::Cursor::Unavailable) .await; } else { let messages = mounted .batch( &mut app, &[key(keyboard::key::Named::F6), remove_key(false)], mouse::Cursor::Unavailable, ) .await; assert!( !messages.iter().any(removal), "F6 entry completes after the batch, before a later Delete" ); } assert!( focused(&mut mounted, &app, "viewport"), "{boundary:?} supports keyboard-only return to the viewport" ); delete(&mut mounted, &mut app, false).await; send(&mut mounted, &mut app, Message::CancelDelete).await; assert_eq!(app.document.bundle(), &original); } select(&mut mounted, &mut app, target).await; let messages = mounted .batch( &mut app, &[ Input::Window(iced::window::Event::Unfocused), remove_key(false), ], mouse::Cursor::Unavailable, ) .await; assert!(!messages.iter().any(removal)); assert!(!app.delete_pending); // A new actual click reacquires ownership after each boundary. select(&mut mounted, &mut app, target).await; delete(&mut mounted, &mut app, false).await; send(&mut mounted, &mut app, Message::CancelDelete).await; assert_eq!(app.document.bundle(), &original); } } #[tokio::test] async fn mounted_behavior_clean_inspector_click_and_tab_own_delete_then_f6_returns_to_canvas() { for process in [false, true] { for tab_entry in [false, true] { let (mut app, target) = if process { let f = topology::direct_tests::fixture(Layer::Oa); (app(f.p, f.owner), f.nodes[0]) } else { let f = scenario::direct_tests::fixture(); (app(f.project, f.owner), f.call) }; let mut mounted = Mounted::new(&mut app).await; select(&mut mounted, &mut app, target).await; let original = app.document.bundle().clone(); let original_name = app.draft.as_ref().unwrap().inputs["/name"].raw.clone(); // The first text input in the right-hand Inspector is the name. // Verify its actual binding below, rather than seeding focus state. let field = 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)) .expect("visible Inspector name input"); if tab_entry { // Canvas Tab stays within canvas controls. Escape then ordinary // Tab follows the existing Inspector entry policy. mounted .batch( &mut app, &[key(keyboard::key::Named::Escape)], mouse::Cursor::Unavailable, ) .await; mounted .batch( &mut app, &[key(keyboard::key::Named::Tab)], mouse::Cursor::Unavailable, ) .await; } else { mounted.click(&mut app, field.center()).await; } assert!( mounted .geometry(&app) .focused .iter() .any(|(_, bounds, value)| *bounds == field && *value), "actual {} focuses the Inspector name", if tab_entry { "Tab" } else { "click" } ); assert!(!focused(&mut mounted, &app, "viewport")); assert!(!app.draft_dirty()); let messages = mounted .batch( &mut app, &[key(keyboard::key::Named::End), remove_key(true)], mouse::Cursor::Unavailable, ) .await; assert!(messages.iter().any(|message| matches!( message, Message::Field(path, value) if path == "/name" && *value == original_name ))); assert!(!messages.iter().any(removal)); assert!(!app.delete_pending); assert!(!app.draft_dirty(), "Delete at end keeps the field clean"); assert_eq!(app.document.bundle(), &original); mounted .batch( &mut app, &[key(keyboard::key::Named::F6)], mouse::Cursor::Unavailable, ) .await; assert!(focused(&mut mounted, &app, "viewport")); assert_eq!( mounted .geometry(&app) .focused .iter() .filter(|(_, _, value)| *value) .count(), 1, "F6 releases actual Inspector focus" ); delete(&mut mounted, &mut app, false).await; send(&mut mounted, &mut app, Message::CancelDelete).await; assert_eq!(app.document.bundle(), &original); } } } #[tokio::test] async fn mounted_behavior_clean_text_focus_and_modifiers_cannot_delete_selected_occurrence() { use iced::advanced::widget::{Operation, operation}; struct FocusText(iced::Rectangle); impl Operation for FocusText { fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) { visit(self); } fn focusable( &mut self, _: Option<&iced::advanced::widget::Id>, bounds: iced::Rectangle, state: &mut dyn operation::Focusable, ) { if bounds == self.0 { state.focus(); } else { state.unfocus(); } } } for process in [false, true] { let (mut app, target) = if process { let f = topology::direct_tests::fixture(Layer::Oa); (app(f.p, f.owner), f.nodes[0]) } else { let f = scenario::direct_tests::fixture(); (app(f.project, f.owner), f.call) }; let mut mounted = Mounted::new(&mut app).await; select(&mut mounted, &mut app, target).await; let original = app.document.bundle().clone(); for modifiers in [ keyboard::Modifiers::CTRL, keyboard::Modifiers::ALT, keyboard::Modifiers::LOGO, ] { let mut event = remove_key(false); if let Input::Keyboard(keyboard::Event::KeyPressed { modifiers: value, .. }) = &mut event { *value = modifiers; } let messages = mounted .batch(&mut app, &[event], mouse::Cursor::Unavailable) .await; assert!(!messages.iter().any(removal)); } let field = mounted .geometry(&app) .text_inputs .into_iter() .find(|bounds| bounds.width > 40.) .expect("real text field"); mounted.operate(&app, &mut FocusText(field)); assert!(!app.draft_dirty()); let messages = mounted .batch(&mut app, &[remove_key(true)], mouse::Cursor::Unavailable) .await; assert!(!messages.iter().any(removal)); assert!(!app.delete_pending); assert_eq!(app.document.bundle(), &original); } } #[tokio::test] async fn mounted_scenario_fragment_and_branch_delete_only_owned_occurrences() { for whole_fragment in [false, true] { let (f, branch, anchor, state, reused, remote) = scenario::direct_tests::placement_fixture(); let fragment = f.project.elements[&branch].parent.unwrap(); let target = if whole_fragment { fragment } else { branch }; let mut app = app(f.project.clone(), f.owner); let mut mounted = Mounted::new(&mut app).await; let original = app.document.bundle().clone(); select(&mut mounted, &mut app, target).await; let token = delete(&mut mounted, &mut app, false).await; send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await; assert!(!app.project().contains(target)); assert!(!app.project().contains(anchor)); assert!(!app.project().contains(branch)); assert_eq!(app.project().contains(fragment), !whole_fragment); for id in [ f.call, f.reply, f.sender, f.receiver, f.exchange, state, reused, remote, ] { assert!(app.project().contains(id)); } consistent(&app); send(&mut mounted, &mut app, Message::Undo).await; assert_eq!(app.document.bundle(), &original); } } #[tokio::test] async fn mounted_qualified_process_child_delete_refuses_outer_reference_and_shared_definition() { let f = topology::direct_tests::fixture(Layer::Oa); let mut p = f.p; let caller = p .add_element(Kind::Process, Layer::Oa, "Caller", None) .unwrap(); let references = ["First use", "Second use"].map(|name| { let mut element = p .make_element(Kind::Occurrence, Layer::Oa, name, Some(caller)) .unwrap(); *element.shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(f.owner), }, precondition: String::new(), postcondition: String::new(), }; let id = element.id; p = execute_batch(&p, &[Command::CreateElement(element)]).unwrap(); id }); let mut app = app(p, caller); let mut mounted = Mounted::new(&mut app).await; let original = app.document.bundle().clone(); let endpoint = OccurrenceEndpoint { references: vec![references[0]], occurrence: f.nodes[0], }; let bounds = mounted.geometry(&app).bounds("process-viewport"); let local = app.behavior .topology .removal_test_point(&app, caller, references[0], Some(&endpoint)); let messages = mounted .click(&mut app, bounds.position() + Vector::new(local.x, local.y)) .await; assert!(messages.iter().any(|message| matches!( message, Message::DiagramSelection(multiselect::Event::Choose( _, crate::diagram_usage::Target::ProcessOccurrence(actual), multiselect::Choice::Replace )) if actual == &endpoint ))); assert_eq!(app.selected, f.nodes[0]); assert!(app.draft.is_none(), "Qualified inspection stays read-only"); assert_eq!( app.behavior.topology.selection_address().unwrap().endpoint, endpoint ); assert!( app.behavior .topology .has_qualified_selection(app.project(), caller) ); mounted .batch(&mut app, &[remove_key(false)], mouse::Cursor::Unavailable) .await; assert!(!app.delete_pending); assert_eq!(app.document.bundle(), &original); assert!(app.status.contains("Open the referenced definition")); select(&mut mounted, &mut app, references[0]).await; assert!( !app.behavior .topology .has_qualified_selection(app.project(), caller) ); let token = delete(&mut mounted, &mut app, true).await; send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await; assert!(!app.project().contains(references[0])); assert!(app.project().contains(references[1])); assert!(app.project().contains(f.owner)); assert!(f.nodes.iter().all(|id| app.project().contains(*id))); consistent(&app); send(&mut mounted, &mut app, Message::Undo).await; assert_eq!(app.document.bundle(), &original); } #[tokio::test] async fn mounted_behavior_pan_and_stale_removal_intent_do_not_delete() { for process in [false, true] { let (mut app, target) = if process { let f = topology::direct_tests::fixture(Layer::Oa); (app(f.p, f.owner), f.nodes[0]) } else { let f = scenario::direct_tests::fixture(); (app(f.project, f.owner), f.call) }; let mut mounted = Mounted::new(&mut app).await; select(&mut mounted, &mut app, target).await; let original = app.document.bundle().clone(); let cursor = point(&mut mounted, &app, target); let messages = mounted .batch( &mut app, &[ Input::Mouse(mouse::Event::CursorMoved { position: cursor }), Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)), remove_key(false), Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)), ], mouse::Cursor::Available(cursor), ) .await; assert!( !messages.iter().any(removal), "held pan never becomes an occurrence deletion" ); assert!(!app.delete_pending); select(&mut mounted, &mut app, target).await; let messages = mounted.input(&app, &[remove_key(false)], mouse::Cursor::Unavailable); let stale = messages .into_iter() .find(removal) .expect("captured live removal"); app.canvas_epoch = app.canvas_epoch.wrapping_add(1); send(&mut mounted, &mut app, stale).await; assert!(!app.delete_pending); assert_eq!(app.document.bundle(), &original); } }