use super::*; use crate::diagram::{DiagramCanvas, State as CanvasState}; use iced::{ mouse, widget::canvas::{self, Program}, }; #[path = "allocation_drag_full_tree_tests.rs"] mod full_tree; #[path = "allocation_drag_tests/group.rs"] mod group; #[path = "allocation_drag_tests/navigation_union.rs"] mod navigation_union; struct Fixture { app: App, a: Id, b: Id, work: Id, node: Id, diagram: Id, } fn xy(x: f32, y: f32) -> Position { Position { x, y } } fn bounds() -> Rectangle { Rectangle::new(Point::new(20., 30.), iced::Size::new(2200., 1400.)) } fn screen(p: Position) -> Point { Point::new(bounds().x + p.x, bounds().y + p.y) } fn fixture(layer: Layer) -> Fixture { let mut p = Project::empty(); let (performer, other, work) = if layer == Layer::Oa { (Kind::Entity, Kind::Actor, Kind::Activity) } else { (Kind::System, Kind::SystemActor, Kind::Function) }; let a = p.add_element(performer, layer, "A", None).unwrap(); let b = p.add_element(other, layer, "B", None).unwrap(); let work = p.add_element(work, layer, "Work", None).unwrap(); let diagram = p.ensure_diagram(layer); p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation; p.place(diagram, a, xy(100., 100.)); p.place(diagram, b, xy(900., 100.)); let node = p.place(diagram, work, xy(100., 500.)); let mut app = App::with_project(p); app.active_diagram = Some(diagram); app.zoom = 1.; app.pan = Vector::ZERO; app.fit_to_view = false; app.reconcile_multiselect(); Fixture { app, a, b, work, node, diagram, } } fn canvas(app: &App) -> DiagramCanvas<'_> { DiagramCanvas { selection: app.diagram_selection.as_ref(), allocation: Some(app.allocation_capability()), project: app.project(), view_index: Some(app.document.view_index()), 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, selected_attachment: None, editable: !app.draft_dirty() && !app.source_dirty(), placing: false, connection: None, } } fn pointer( canvas: &DiagramCanvas<'_>, state: &mut CanvasState, event: mouse::Event, final_cursor: Point, ) -> Option { canvas .update( state, &canvas::Event::Mouse(event), bounds(), mouse::Cursor::Available(final_cursor), ) .and_then(|a| a.into_inner().0) } fn start(f: &Fixture) -> Point { let p = f.app.project().diagrams[&f.diagram].nodes[&f.node].position; screen(xy(p.x + 40., p.y + 35.)) } fn batch(app: &App, from: Point, to: Point) -> Vec { let canvas = canvas(app); let mut state = CanvasState::default(); [ mouse::Event::CursorMoved { position: from }, mouse::Event::ButtonPressed(mouse::Button::Left), mouse::Event::CursorMoved { position: to }, mouse::Event::ButtonReleased(mouse::Button::Left), ] .into_iter() .filter_map(|e| pointer(&canvas, &mut state, e, to)) .collect() } fn send(app: &mut App, events: Vec) { for e in events { let _ = app.update(e); } } fn press(f: &Fixture) -> Arc { batch(&f.app, start(f), start(f)) .into_iter() .find_map(|m| match m { Message::WorkDrag(Event::Press(p)) => Some(p), _ => None, }) .expect("actual clean work-body press") } fn admit_press(f: &mut Fixture) -> Arc { let p = press(f); let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone()))); assert!(f.app.allocation_drag.active.is_some(), "{}", f.app.status); p } fn singleton(candidate: &Candidate) -> &Arc { let Candidate::Singleton(plan) = candidate else { panic!("legacy singleton candidate") }; plan } fn drag(f: &mut Fixture, to: Point) { let events = batch(&f.app, start(f), to); assert!(matches!( events.first(), Some(Message::WorkDrag(Event::Press(_))) )); assert!(events.iter().all(|m| matches!(m, Message::WorkDrag(_)))); send(&mut f.app, events); } #[test] fn mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let original = f.app.project().clone(); let original_source = f.app.document.bundle().clone(); drag(&mut f, screen(xy(160., 190.))); assert_eq!(f.app.document.revision(), 1, "{}", f.app.status); let relation = f .app .project() .relations .values() .find(|r| r.kind() == RelationKind::Allocation) .unwrap() .clone(); assert_eq!((relation.source(), relation.target()), (f.a, f.work)); assert_eq!(f.app.project().elements, original.elements); let after_in = f.app.document.bundle().clone(); let _ = f.app.update(Message::Undo); assert_eq!(f.app.document.bundle(), &original_source); assert!(!f.app.document.can_undo()); let _ = f.app.update(Message::Redo); assert_eq!(f.app.document.bundle(), &after_in); drag(&mut f, screen(xy(960., 190.))); let reassigned = &f.app.project().relations[&relation.id]; assert_eq!(reassigned.source(), f.b, "{}", f.app.status); assert_eq!(reassigned.name, relation.name); assert_eq!(reassigned.description, relation.description); let before_out = f.app.document.bundle().clone(); drag(&mut f, screen(xy(600., 700.))); assert!( !f.app.project().relations.contains_key(&relation.id), "{}", f.app.status ); assert_eq!(f.app.project().elements, original.elements); let after_out = f.app.document.bundle().clone(); assert_eq!( oa_core::source_project::compile_bundle(&after_out, Default::default()) .unwrap() .decoded .project, *f.app.project() ); let _ = f.app.update(Message::Undo); assert_eq!(f.app.document.bundle(), &before_out); let _ = f.app.update(Message::Redo); assert_eq!(f.app.document.bundle(), &after_out); } } #[test] fn mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply() { let mut f = fixture(Layer::Oa); let before = f.app.document.snapshot(); let events = batch(&f.app, start(&f), start(&f)); send(&mut f.app, events); assert_eq!(f.app.selected, f.work); assert_eq!(f.app.selected_node, Some(f.node)); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert!(!f.app.document.can_undo()); assert!(f.app.allocation_drag.active.is_none()); } #[test] fn mounted_dirty_property_press_does_not_select_apply_or_destroy_draft() { for same_selection in [false, true] { let mut f = fixture(Layer::Oa); if same_selection { let _ = f.app.update(Message::Select(f.work)); } let _ = f.app.update(Message::Field( "/name".into(), "Keep my unfinished name".into(), )); let selection = (f.app.selected, f.app.selected_node); let before = f.app.document.snapshot(); let dirty = f.app.draft.as_ref().unwrap().value.clone(); let input = &f.app.draft.as_ref().unwrap().inputs["/name"]; let raw = input.raw.clone(); let cursor = input.editor.cursor(); let events = batch(&f.app, start(&f), screen(xy(160., 190.))); assert!(matches!( events.first(), Some(Message::WorkDrag(Event::Refused(..))) )); assert!(!events.iter().any(|m| matches!( m, Message::Select(_) | Message::SelectNode(_) | Message::MoveNode(..) ))); send(&mut f.app, events); assert_eq!((f.app.selected, f.app.selected_node), selection); assert_eq!(f.app.draft.as_ref().unwrap().value, dirty); assert_eq!(f.app.draft.as_ref().unwrap().inputs["/name"].raw, raw); assert_eq!( f.app.draft.as_ref().unwrap().inputs["/name"] .editor .cursor(), cursor ); assert!(f.app.draft_dirty()); assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); assert_eq!(f.app.status, FINISH_EDIT); } } #[test] fn source_and_modal_admission_reject_prebuilt_clean_press_without_selecting() { for mode in 0..4 { let mut f = fixture(Layer::Oa); let p = press(&f); let selection = f.app.selected; let source = f.app.document.bundle().clone(); match mode { 0 => { let _ = f.app.update(Message::SourceEdit( iced::widget::text_editor::Action::Edit( iced::widget::text_editor::Edit::Insert('x'), ), )); } 1 => f.app.busy = true, 2 => f.app.delete_pending = true, _ => f.app.hosted.visible = true, } let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone()))); let _ = f.app.update(Message::WorkDrag(Event::Release( p.stamp, p.token, Some(screen(xy(160., 190.))), ))); assert_eq!(f.app.selected, selection); assert_eq!(f.app.document.bundle(), &source); assert!(!f.app.document.can_undo()); assert!(f.app.allocation_drag.active.is_none()); } } #[test] fn modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay() { for interruption in 0..6 { let mut f = fixture(Layer::Oa); let p = press(&f); let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone()))); let before = f.app.document.bundle().clone(); match interruption { 0 => { f.app.delete_pending = true; let _ = f.app.update(Message::CancelDelete); } 1 => { let _ = f.app.update(Message::Fit); f.app.zoom = p.stamp.zoom; f.app.pan = p.stamp.pan; } 2 => { let _ = f.app.update(Message::Undo); } 3 => { let _ = f .app .update(Message::CanvasResized(iced::Size::new(2300., 1400.))); f.app.canvas_size = None; } 4 => { let _ = f.app.update(Message::WorkDrag(Event::Unfocused)); } _ => { let _ = f.app.update(Message::Escape); } } let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone()))); let _ = f.app.update(Message::WorkDrag(Event::Release( p.stamp, p.token, Some(screen(xy(160., 190.))), ))); assert_eq!(f.app.document.bundle(), &before); assert!(!f.app.document.can_undo()); assert!(f.app.allocation_drag.active.is_none()); } } #[test] fn stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once() { let mut f = fixture(Layer::Oa); let old = press(&f); let _ = f.app.update(Message::WorkDrag(Event::Press(old.clone()))); let _ = f.app.update(Message::Escape); let new = press(&f); let _ = f.app.update(Message::WorkDrag(Event::Press(new.clone()))); let _ = f.app.update(Message::WorkDrag(Event::Preview( old.stamp, old.token, Some(screen(xy(160., 190.))), ))); assert_eq!( f.app.allocation_drag.active.as_ref().unwrap().press.token, new.token ); let release = Message::WorkDrag(Event::Release( new.stamp, new.token, Some(screen(xy(160., 190.))), )); let _ = f.app.update(release.clone()); let revision = f.app.document.revision(); let _ = f.app.update(release); assert_eq!(f.app.document.revision(), revision); assert_eq!(revision, 1, "{}", f.app.status); } #[test] fn arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source() { let mut f = fixture(Layer::Oa); let _ = f.app.update(Message::Command(Command::Rename( f.work, "Temporary".into(), ))); let _ = f.app.update(Message::Undo); let before = f.app.document.bundle().clone(); let revision = f.app.document.revision(); let _ = f.app.update(Message::WorkDrag(Event::ArrangeNext)); assert!(f.app.allocation_drag.arrange_next); let p = press(&f); let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone()))); assert_eq!(p.mode, Mode::Arrange); assert!(!f.app.allocation_drag.arrange_next); let _ = f.app.update(Message::WorkDrag(Event::Release( p.stamp, p.token, Some(p.start + Vector::new(4., 0.)), ))); assert_eq!(f.app.document.bundle(), &before); assert_eq!(f.app.document.revision(), revision); assert!(f.app.document.can_redo()); assert!(f.app.project().relations.is_empty()); assert_eq!(press(&f).mode, Mode::Assign); } #[test] fn outside_release_is_cancel_and_does_not_remove_an_assignment() { let mut f = fixture(Layer::Oa); drag(&mut f, screen(xy(160., 190.))); let before = f.app.document.bundle().clone(); let revision = f.app.document.revision(); drag( &mut f, Point::new(bounds().x + bounds().width, bounds().y + 300.), ); assert_eq!(f.app.document.bundle(), &before); assert_eq!(f.app.document.revision(), revision); } #[test] fn candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index() { let mut f = fixture(Layer::Oa); let p = press(&f); let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone()))); let a = screen(xy(160., 190.)); let b = screen(xy(960., 190.)); let _ = f .app .update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(a)))); let first = singleton( &f.app .allocation_drag .preview .as_ref() .expect(&f.app.status) .candidate, ) .clone(); let stable = first .project() .relations .values() .find(|r| r.kind() == RelationKind::Allocation) .unwrap() .id; let _ = f.app.update(Message::WorkDrag(Event::Preview( p.stamp, p.token, Some(a + Vector::new(1., 0.)), ))); assert!(Arc::ptr_eq( &first, singleton(&f.app.allocation_drag.preview.as_ref().unwrap().candidate) )); let _ = f .app .update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(b)))); let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status); assert!(!Arc::ptr_eq(&first, singleton(&preview.candidate))); assert_eq!(preview.project().relations[&stable].source(), f.b); assert_eq!( preview.placement.view, projection::project_diagram_with_index( preview.project(), &preview.placement.diagram, preview.index() ) ); assert_ne!( preview.placement.view, projection::project_diagram_with_index( f.app.project(), &preview.placement.diagram, f.app.document.view_index() ) ); let _ = f .app .update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(a)))); assert!(Arc::ptr_eq( &first, singleton(&f.app.allocation_drag.preview.as_ref().unwrap().candidate) )); assert_eq!( f.app.allocation_drag.active.as_ref().unwrap().cache.len(), 2 ); } #[test] fn frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap() { let mut f = fixture(Layer::Oa); let p = admit_press(&mut f); let mut frozen = f .app .allocation_drag .active .as_ref() .unwrap() .frozen .as_ref() .unwrap() .as_ref() .clone(); let ai = frozen .targets .iter() .position(|t| t.element == f.a) .unwrap(); let bi = frozen .targets .iter() .position(|t| t.element == f.b) .unwrap(); frozen.targets[ai].region = Rectangle::new(Point::new(100., 100.), iced::Size::new(200., 200.)); frozen.targets[bi].region = Rectangle::new(Point::new(300., 100.), iced::Size::new(200., 200.)); assert!( matches!(frozen.resolve(p.stamp, Some(screen(xy(300., 110.))), false), Hit::Destination(plan::Destination::Performer(id), _) if id == f.b) ); assert!(matches!( frozen.resolve(p.stamp, Some(screen(xy(500., 110.))), false), Hit::Destination(plan::Destination::OutsidePerformers, None) )); frozen.targets[bi].region = Rectangle::new(Point::new(150., 150.), iced::Size::new(50., 50.)); assert!(matches!( frozen.resolve(p.stamp, Some(screen(xy(160., 160.))), false), Hit::Ambiguous(_) )); frozen.targets[bi].ancestors.push(f.a); assert!( matches!(frozen.resolve(p.stamp, Some(screen(xy(160., 160.))), false), Hit::Destination(plan::Destination::Performer(id), _) if id == f.b) ); frozen.cards.push(Rectangle::new( Point::new(160., 160.), iced::Size::new(20., 20.), )); assert_eq!( frozen.resolve(p.stamp, Some(screen(xy(160., 160.))), false), Hit::Occluded ); assert_eq!(frozen.resolve(p.stamp, None, false), Hit::OutsideViewport); assert_eq!( frozen.resolve( p.stamp, Some(Point::new(bounds().x + 50., bounds().y + 15.)), false ), Hit::Destination(plan::Destination::OutsidePerformers, None) ); } #[test] fn release_uses_ordered_native_position_before_later_motion_in_same_batch() { let mut f = fixture(Layer::Oa); let from = start(&f); let a = screen(xy(160., 190.)); let b = screen(xy(960., 190.)); let mut state = CanvasState::default(); let canvas = canvas(&f.app); let events: Vec<_> = [ mouse::Event::CursorMoved { position: from }, mouse::Event::ButtonPressed(mouse::Button::Left), mouse::Event::CursorMoved { position: a }, mouse::Event::ButtonReleased(mouse::Button::Left), mouse::Event::CursorMoved { position: b }, ] .into_iter() .filter_map(|e| pointer(&canvas, &mut state, e, b)) .collect(); send(&mut f.app, events); assert_eq!( validation::effective_performer(f.app.project(), f.work), Some(f.a), "{}", f.app.status ); } fn replace(f: &mut Fixture, project: Project) { f.app.document = crate::document::Document::from_project(project, Default::default()).unwrap(); f.app.sync(); f.app.reconcile_multiselect(); } #[test] fn same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses() { let mut f = fixture(Layer::Oa); let mut project = f.app.project().clone(); let diagram = project.diagrams.get_mut(&f.diagram).unwrap(); diagram .nodes .values_mut() .find(|n| n.element == f.b) .unwrap() .position .x = 324.; replace(&mut f, project); let p = press(&f); let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone()))); let left = screen(xy(323.5, 140.)); let right = screen(xy(324.5, 140.)); assert_eq!( ((left.x - p.start.x + 100.) / 10.).round(), ((right.x - p.start.x + 100.) / 10.).round() ); for at in [left, right] { let _ = f.app.update(Message::WorkDrag(Event::Preview( p.stamp, p.token, Some(at), ))); } let active = f.app.allocation_drag.active.as_ref().unwrap(); assert_eq!(active.cache.len(), 2); let a = active .cache .iter() .find(|(d, _)| *d == plan::Destination::Performer(f.a)) .unwrap() .1 .as_ref() .unwrap(); let b = active .cache .iter() .find(|(d, _)| *d == plan::Destination::Performer(f.b)) .unwrap() .1 .as_ref() .unwrap(); assert!(!Arc::ptr_eq(singleton(a), singleton(b))); let stable = a .project() .relations .values() .find(|r| r.kind() == RelationKind::Allocation) .unwrap() .id; assert_eq!(b.project().relations[&stable].source(), f.b); assert!(f.app.project().relations.is_empty()); } #[test] fn source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision() { let mut f = fixture(Layer::Oa); let before = f.app.document.bundle().clone(); let mut limits = oa_core::source_project::ProjectLimits::default(); limits.model.max_source_bytes = before.files.values().map(String::len).sum(); f.app.document = crate::document::Document::from_bundle(before.clone(), limits).unwrap(); f.app.sync(); let snapshot = f.app.document.snapshot(); drag(&mut f, screen(xy(160., 190.))); assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot())); assert_eq!(f.app.document.bundle(), &before); assert!(!f.app.document.can_undo()); assert!(f.app.status.contains("retry"), "{}", f.app.status); assert!(f.app.allocation_drag.active.is_none()); f.app.document = crate::document::Document::from_bundle(before, Default::default()).unwrap(); f.app.sync(); drag(&mut f, screen(xy(160., 190.))); assert_eq!( validation::effective_performer(f.app.project(), f.work), Some(f.a), "{}", f.app.status ); assert_eq!(f.app.document.revision(), 1); } #[test] fn mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation() { let mut f = fixture(Layer::Oa); let mut project = f.app.project().clone(); let role = project .add_element(Kind::Role, Layer::Oa, "Responder", None) .unwrap(); project.relate(RelationKind::RoleAssignment, "Responsible", role, f.work); project.relate(RelationKind::RoleRealization, "Realizes", f.a, role); project .diagrams .get_mut(&f.diagram) .unwrap() .nodes .get_mut(&f.node) .unwrap() .position = xy(150., 200.); replace(&mut f, project); let relations = f.app.project().relations.clone(); let source = f.app.document.bundle().clone(); drag(&mut f, screen(xy(650., 700.))); assert_eq!(f.app.document.bundle(), &source); assert!(!f.app.document.can_undo()); assert!( f.app.status.to_lowercase().contains("role"), "{}", f.app.status ); let admitted = admit_press(&mut f); let target = f .app .allocation_drag .active .as_ref() .unwrap() .frozen .as_ref() .unwrap() .targets .iter() .find(|t| t.element == f.a) .unwrap() .region; let _ = f .app .update(Message::WorkDrag(Event::Cancel(admitted.token))); drag(&mut f, screen(xy(target.x + 45., target.y + 100.))); assert_eq!(f.app.document.revision(), 1, "{}", f.app.status); assert_eq!(f.app.project().relations, relations); assert!( !f.app .project() .relations .values() .any(|r| r.kind() == RelationKind::Allocation) ); } #[test] fn mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves() { let mut f = fixture(Layer::Oa); let mut project = f.app.project().clone(); let child = project .add_element(Kind::Activity, Layer::Oa, "Child", Some(f.work)) .unwrap(); let hidden = project.place(f.diagram, child, xy(150., 600.)); project .diagrams .get_mut(&f.diagram) .unwrap() .nodes .get_mut(&f.node) .unwrap() .collapsed = true; replace(&mut f, project); let before = f.app.project().diagrams[&f.diagram].nodes.clone(); drag(&mut f, screen(xy(960., 190.))); assert_eq!(f.app.document.revision(), 1, "{}", f.app.status); let after = &f.app.project().diagrams[&f.diagram].nodes; let dx = after[&f.node].position.x - before[&f.node].position.x; let dy = after[&f.node].position.y - before[&f.node].position.y; assert_eq!( after[&hidden].position, xy( before[&hidden].position.x + dx, before[&hidden].position.y + dy ) ); assert!(after[&f.node].collapsed); let assigned: Vec<_> = f .app .project() .relations .values() .filter(|r| r.kind() == RelationKind::Allocation) .collect(); assert_eq!(assigned.len(), 1); assert_eq!((assigned[0].source(), assigned[0].target()), (f.b, child)); assert_eq!(f.app.project().elements[&child].parent, Some(f.work)); } #[test] fn mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection() { let mut f = fixture(Layer::Sa); let mut project = f.app.project().clone(); let port = Id::new_v4(); project.elements.insert( port, make_port( &project, port, f.work, "Send", PortContract::FunctionOutput { outgoing_items: vec![], }, ) .unwrap(), ); replace(&mut f, project); let view = projection::project_view(f.app.project(), f.diagram); let at = screen( view.ports .iter() .find(|p| p.element == port) .unwrap() .position, ); let _ = f .app .update(Message::Field("/name".into(), "Keep dirty name".into())); let selection = f.app.selected; let before = f.app.document.bundle().clone(); let events = batch(&f.app, at, at + Vector::new(30., 0.)); assert!(matches!( events.first(), Some(Message::WorkDrag(Event::Refused(..))) )); send(&mut f.app, events); assert_eq!(f.app.selected, selection); assert_eq!(f.app.document.bundle(), &before); assert!(f.app.draft_dirty()); } #[test] fn frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude() { let mut f = fixture(Layer::Oa); let mut project = f.app.project().clone(); project .diagrams .get_mut(&f.diagram) .unwrap() .nodes .values_mut() .find(|n| n.element == f.a) .unwrap() .collapsed = true; replace(&mut f, project); let p = admit_press(&mut f); let mut frozen = f .app .allocation_drag .active .as_ref() .unwrap() .frozen .as_ref() .unwrap() .as_ref() .clone(); let t = frozen.targets.iter().find(|t| t.element == f.a).unwrap(); let bottom = xy(t.region.x + 30., t.region.y + t.region.height); assert!(t.region.height <= 62.); assert_eq!( frozen.resolve(p.stamp, Some(screen(bottom)), false), Hit::Destination(plan::Destination::OutsidePerformers, None) ); frozen.controls.push(Rectangle::new( Point::new(500., 500.), iced::Size::new(20., 20.), )); assert_eq!( frozen.resolve(p.stamp, Some(screen(xy(500., 500.))), false), Hit::Occluded ); frozen .edges .push((Point::new(600., 600.), Point::new(700., 600.))); assert_eq!( frozen.resolve(p.stamp, Some(screen(xy(650., 605.))), false), Hit::Occluded ); } #[test] fn pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet() { let mut f = fixture(Layer::Oa); let p = press(&f); let project = f.app.project().clone(); let _ = f.app.behavior.update( &project, crate::behavior::Event::Creation(crate::behavior::creation::Event::Open(Layer::Oa)), ); assert!(f.app.behavior.has_pending()); let events = batch(&f.app, start(&f), screen(xy(160., 190.))); assert!(matches!( events.first(), Some(Message::WorkDrag(Event::Refused(..))) )); let source = f.app.document.bundle().clone(); send(&mut f.app, events); assert!(f.app.behavior.has_pending()); assert_eq!(f.app.document.bundle(), &source); let _ = f.app.behavior.update( &project, crate::behavior::Event::Creation(crate::behavior::creation::Event::Cancel), ); replace(&mut f, project); let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone()))); let _ = f.app.update(Message::WorkDrag(Event::Release( p.stamp, p.token, Some(screen(xy(160., 190.))), ))); assert_eq!(f.app.document.bundle(), &source); assert!(!f.app.document.can_undo()); } #[test] fn actual_canvas_origin_change_cancels_held_work_before_any_legacy_release() { let mut f = fixture(Layer::Oa); let from = start(&f); let mut state = CanvasState::default(); let events = { let canvas = canvas(&f.app); let mut events = Vec::new(); for event in [ mouse::Event::CursorMoved { position: from }, mouse::Event::ButtonPressed(mouse::Button::Left), ] { if let Some(message) = pointer(&canvas, &mut state, event, from) { events.push(message); } } let moved_bounds = Rectangle { x: bounds().x + 1., ..bounds() }; let canceled = canvas .update( &mut state, &canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), moved_bounds, mouse::Cursor::Available(screen(xy(160., 190.))), ) .and_then(|a| a.into_inner().0) .unwrap(); assert!(matches!(canceled, Message::WorkDrag(Event::Cancel(_)))); events.push(canceled); events }; let source = f.app.document.bundle().clone(); send(&mut f.app, events); assert_eq!(f.app.document.bundle(), &source); assert!(!f.app.document.can_undo()); assert!(f.app.allocation_drag.active.is_none()); } #[test] fn toolbar_status_leaves_transformed_top_edge_work_header_readable() { for zoom in [0.75, 1.25] { let mut f = fixture(Layer::Oa); f.app.zoom = zoom; f.app.pan = Vector::new(20. * zoom, 20. * zoom); let screen = |world: Position| { Point::new( bounds().x + (world.x + 20.) * zoom, bounds().y + (world.y + 20.) * zoom, ) }; let from = screen(xy(140., 580.)); // 80-unit offset: release pointer clears the old band. let to = screen(xy(640., 60.)); let mut events = batch(&f.app, from, to); let release = events.pop().unwrap(); send(&mut f.app, events); let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status); let header = preview .placement .view .nodes .iter() .find(|n| n.id == f.node) .unwrap(); assert_eq!(header.position.y, -20.); assert!((screen(header.position).y - bounds().y).abs() < 0.01); assert!( f.app .allocation_drag .feedback .as_ref() .unwrap() .targets .is_empty() ); let _ = f.app.update(release); assert_eq!(f.app.document.revision(), 1, "{}", f.app.status); } } #[test] fn toolbar_destination_names_transformed_target_whose_header_is_clipped() { for zoom in [0.75, 1.25] { let mut f = fixture(Layer::Oa); let mut project = f.app.project().clone(); let target = project .diagrams .get_mut(&f.diagram) .unwrap() .nodes .values_mut() .find(|n| n.element == f.a) .unwrap(); target.position.y = -41.; target.size.height = 300.; replace(&mut f, project); f.app.zoom = zoom; f.app.pan = Vector::new(20. * zoom, 20. * zoom); let screen = |world: Position| { Point::new( bounds().x + (world.x + 20.) * zoom, bounds().y + (world.y + 20.) * zoom, ) }; let mut events = batch(&f.app, screen(xy(140., 535.)), screen(xy(200., 80.))); let release = events.pop().unwrap(); send(&mut f.app, events); let feedback = f.app.allocation_drag.feedback.as_ref().unwrap(); assert!(!feedback.refused, "{}", feedback.text); let target = &feedback.frozen.as_ref().unwrap().targets[feedback.targets[0]]; assert!(screen(xy(target.region.x, target.region.y)).y < bounds().y); assert_eq!(feedback.destination_identity(), "A"); assert!(f.app.allocation_drag.preview.is_some()); let _ = f.app.update(release); assert_eq!( validation::effective_performer(f.app.project(), f.work), Some(f.a) ); } } #[test] fn oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history() { for layer in [Layer::Oa, Layer::Sa] { for (zoom, pan) in [ (1., Vector::ZERO), (0.75, Vector::new(37., -15.)), (1.25, Vector::new(-30., 23.)), ] { let mut f = fixture(layer); let mut project = f.app.project().clone(); project.elements.get_mut(&f.a).unwrap().name = "Receiver".into(); replace(&mut f, project); // Refusals must also preserve an existing redo branch. let _ = f.app.update(Message::Command(Command::Rename( f.work, "Temporary".into(), ))); let _ = f.app.update(Message::Undo); let pan = Vector::new(pan.x, pan.y - 100. * zoom); f.app.zoom = zoom; f.app.pan = pan; f.app.fit_to_view = false; let screen = |world: Position| { Point::new( bounds().x + world.x * zoom + pan.x, bounds().y + world.y * zoom + pan.y, ) }; let from = screen(xy(140., 535.)); // Keep the pointer inside the visible target even when the camera // clips its top. Only the grabbed work header must be clipped. let overlap = screen(xy(160., 110_f32.max(-pan.y / zoom + 5.))); assert!(bounds().contains(overlap)); let clear = screen(xy(160., 190.)); let accepted = f.app.project().clone(); let source = f.app.document.bundle().clone(); let snapshot = f.app.document.snapshot(); let revision = f.app.document.revision(); let mut events = batch(&f.app, from, overlap); let release = events.pop().unwrap(); send(&mut f.app, events); assert!(f.app.allocation_drag.preview.is_none()); let feedback = f.app.allocation_drag.feedback.as_ref().unwrap(); assert!(feedback.refused, "{}", feedback.text); assert!(feedback.text.contains("Make room or adjust the view")); assert_eq!( feedback.frozen.as_ref().unwrap().targets[feedback.targets[0]].element, f.a ); assert_eq!(f.app.project(), &accepted); assert_eq!(f.app.document.bundle(), &source); assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot())); assert!(!f.app.document.can_undo()); assert!(f.app.document.can_redo()); let _ = f.app.update(release); assert!(f.app.allocation_drag.active.is_none()); assert!(f.app.allocation_drag.preview.is_none()); assert_eq!(f.app.project(), &accepted); assert_eq!(f.app.document.bundle(), &source); assert_eq!(f.app.document.revision(), revision); assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot())); assert!(!f.app.document.can_undo()); assert!(f.app.document.can_redo()); // In a new gesture, moving into the viewport reuses its semantic // candidate and stable allocation identity; only geometry changes. let mut events = batch(&f.app, from, overlap); events.pop(); send(&mut f.app, events); let active = f.app.allocation_drag.active.as_ref().unwrap(); let p = active.press.clone(); let semantic = singleton(active.cache[0].1.as_ref().unwrap()).clone(); let _ = f.app.update(Message::WorkDrag(Event::Preview( p.stamp, p.token, Some(clear), ))); let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status); assert!(Arc::ptr_eq(&semantic, singleton(&preview.candidate))); assert_eq!( preview.placement.diagram.nodes[&f.node].position, xy(120., 160.) ); assert_eq!( preview.placement.view, projection::project_diagram_with_index( semantic.project(), &preview.placement.diagram, semantic.index(), ) ); let mut expected = semantic.project().clone(); expected .diagrams .insert(f.diagram, preview.placement.diagram.clone()); assert_eq!(f.app.project(), &accepted); assert_eq!(f.app.document.bundle(), &source); let _ = f.app.update(Message::WorkDrag(Event::Release( p.stamp, p.token, Some(clear), ))); assert_eq!(f.app.project(), &expected, "{}", f.app.status); assert_eq!(f.app.document.revision(), revision + 1); assert!(f.app.document.can_undo()); assert!(!f.app.document.can_redo()); let after = f.app.document.bundle().clone(); assert_eq!( oa_core::source_project::compile_bundle(&after, Default::default()) .unwrap() .decoded .project, expected ); let _ = f.app.update(Message::Undo); assert_eq!(f.app.project(), &accepted); assert_eq!(f.app.document.bundle(), &source); assert!(!f.app.document.can_undo()); let _ = f.app.update(Message::Redo); assert_eq!(f.app.project(), &expected); assert_eq!(f.app.document.bundle(), &after); } } } #[test] fn oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag() { for layer in [Layer::Oa, Layer::Sa] { for (name, collapsed) in [("Receiver", false), ("Receiving subsystem", true)] { for (zoom, pan) in [ (1., Vector::ZERO), (0.75, Vector::new(37., -15.)), (1.25, Vector::new(-30., 23.)), (1., Vector::new(0., -100.)), ] { let mut f = fixture(layer); let mut project = f.app.project().clone(); project.elements.get_mut(&f.a).unwrap().name = name.into(); let target = project .diagrams .get_mut(&f.diagram) .unwrap() .nodes .values_mut() .find(|n| n.element == f.a) .unwrap(); target.collapsed = collapsed; let target_id = target.id; replace(&mut f, project); f.app.zoom = zoom; f.app.pan = pan; f.app.fit_to_view = false; let screen = |world: Position| { Point::new( bounds().x + world.x * zoom + pan.x, bounds().y + world.y * zoom + pan.y, ) }; let accepted = f.app.project().clone(); let source = f.app.document.bundle().clone(); let snapshot = f.app.document.snapshot(); // Normal name grab (40,35), release inside the original header. let mut events = batch(&f.app, screen(xy(140., 535.)), screen(xy(160., 140.))); let release = events.pop().unwrap(); send(&mut f.app, events); let feedback = f .app .allocation_drag .feedback .as_ref() .expect(&f.app.status); assert!(!feedback.refused, "{}", feedback.text); assert_eq!(feedback.destination_identity(), name); assert_eq!( feedback.frozen.as_ref().unwrap().targets[feedback.targets[0]].placement, target_id ); let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status); assert_eq!( preview.placement.diagram.nodes[&f.node].position, xy(120., 110.) ); assert!(!preview.placement.diagram.nodes[&target_id].collapsed); assert_eq!( preview.placement.view, projection::project_diagram_with_index( preview.project(), &preview.placement.diagram, preview.index() ) ); let mut expected = preview.project().clone(); expected .diagrams .insert(f.diagram, preview.placement.diagram.clone()); assert_eq!(f.app.project(), &accepted); assert_eq!(f.app.document.bundle(), &source); assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot())); assert!(!f.app.document.can_undo()); let _ = f.app.update(release); assert_eq!(f.app.project(), &expected, "{}", f.app.status); assert_eq!(f.app.document.revision(), 1); assert_eq!( validation::effective_performer(f.app.project(), f.work), Some(f.a) ); let after = f.app.document.bundle().clone(); let _ = f.app.update(Message::Undo); assert_eq!(f.app.project(), &accepted); assert_eq!(f.app.document.bundle(), &source); assert!(!f.app.document.can_undo()); let _ = f.app.update(Message::Redo); assert_eq!(f.app.project(), &expected); assert_eq!(f.app.document.bundle(), &after); assert!(!f.app.document.can_redo()); } } } } #[test] fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation() { let f = fixture(Layer::Oa); let p = press(&f); let mut feedback = Feedback { press: p, frozen: None, ordinary_performers: false, targets: vec![], inspection_targets: vec![], text: "A card blocks this drop".into(), refused: true, }; assert_eq!(feedback.destination_identity(), "No eligible destination"); feedback.text = "Release inside the captured canvas".into(); assert_eq!(feedback.destination_identity(), "No eligible destination"); feedback.refused = false; assert_eq!(feedback.destination_identity(), "Outside performers"); }