use super::*; use crate::app::multiselect::{self, Choice}; use crate::diagram_usage::Target as SelectionTarget; use iced::widget::text_editor; #[path = "group/connection_lifetime.rs"] mod connection_lifetime; #[path = "group/route_lifetime.rs"] mod route_lifetime; struct Group { f: Fixture, y: Id, yn: Id, c: Id, cn: Id, an: Id, ax: Id, by: Id, } fn group(layer: Layer) -> Group { let mut f = fixture(layer); let mut p = f.app.project().clone(); let y = p .add_element(p.elements[&f.work].kind(), layer, "Y", None) .unwrap(); let c = p .add_element( if layer == Layer::Oa { Kind::Actor } else { Kind::SystemActor }, layer, "C", None, ) .unwrap(); let yn = p.place(f.diagram, y, xy(140., 600.)); let cn = p.place(f.diagram, c, xy(1500., 100.)); let d = p.diagrams.get_mut(&f.diagram).unwrap(); d.nodes.get_mut(&f.node).unwrap().position = xy(140., 200.); d.nodes .values_mut() .find(|n| n.element == f.b) .unwrap() .position = xy(100., 500.); d.nodes.get_mut(&cn).unwrap().size = Size { width: 600., height: 900., }; let an = d.nodes.values().find(|n| n.element == f.a).unwrap().id; let ax = p.relate( RelationKind::Allocation, "Reviewed X responsibility", f.a, f.work, ); let by = p.relate( RelationKind::Allocation, "Reviewed Y responsibility", f.b, y, ); p.relations.get_mut(&ax).unwrap().description = "Keep X provenance".into(); p.relations.get_mut(&by).unwrap().description = "Keep Y provenance".into(); validate_candidate(&p).unwrap(); f.app = App::with_project(p); f.app.active_diagram = Some(f.diagram); f.app.zoom = 1.; f.app.pan = Vector::ZERO; f.app.fit_to_view = false; f.app.reconcile_multiselect(); Group { f, y, yn, c, cn, an, ax, by, } } fn choose(app: &mut App, target: SelectionTarget, choice: Choice) { let context = multiselect::context(app.route_context().unwrap()); let _ = app.update(Message::DiagramSelection(multiselect::Event::Choose( context, target, choice, ))); } fn select(app: &mut App, ids: &[Id]) { for (i, id) in ids.iter().enumerate() { choose( app, SelectionTarget::Placement(*id), if i == 0 { Choice::Replace } else { Choice::Add }, ); } assert_eq!( app.diagram_selection.as_ref().unwrap().targets(), &ids.iter() .copied() .map(SelectionTarget::Placement) .collect::>() ); } fn selection( app: &App, ) -> ( Vec, Option, Id, Option, ) { let s = app.diagram_selection.as_ref().unwrap(); ( s.targets().to_vec(), s.primary().cloned(), app.selected, app.selected_node, ) } fn node_point(app: &App, node: Id) -> Point { let view = projection::project_view(app.project(), app.active_diagram.unwrap()); let n = view.nodes.iter().find(|n| n.id == node).unwrap(); screen(xy(n.position.x + 40., n.position.y + 35.)) } fn input(app: &mut App, state: &mut CanvasState, event: mouse::Event, at: Point) { if let Some(message) = pointer(&canvas(app), state, event, at) { let _ = app.update(message); } } fn begin(app: &mut App, state: &mut CanvasState, from: Point) -> Arc { input( app, state, mouse::Event::CursorMoved { position: from }, from, ); let message = pointer( &canvas(app), state, mouse::Event::ButtonPressed(mouse::Button::Left), from, ) .unwrap(); let Message::WorkDrag(Event::Press(packet)) = &message else { panic!("Allocation placement must enter its captured lifetime: {message:?}"); }; let packet = packet.clone(); let _ = app.update(message); packet } fn move_pointer(app: &mut App, state: &mut CanvasState, to: Point) { input(app, state, mouse::Event::CursorMoved { position: to }, to); } fn release(app: &mut App, state: &mut CanvasState, to: Point) { input( app, state, mouse::Event::ButtonReleased(mouse::Button::Left), to, ); } fn exact_history( app: &mut App, before: &Project, source: &oa_core::source_project::SourceBundle, expected: &Project, ) { assert_eq!(app.project(), expected); let after = app.document.bundle().clone(); let reopened = crate::document::Document::from_bundle(after.clone(), Default::default()).unwrap(); assert_eq!(reopened.project(), expected); let _ = app.update(Message::Undo); assert_eq!(app.project(), before); assert_eq!(app.document.bundle(), source); assert!( !app.document.can_undo(), "one group drag is one history step" ); let _ = app.update(Message::Redo); assert_eq!(app.project(), expected); assert_eq!(app.document.bundle(), &after); assert!(!app.document.can_redo()); } #[test] fn completed_text_apply_rejects_old_allocation_packets_without_retiring_a_new_drag() { for layer in [Layer::Oa, Layer::Sa] { let mut g = group(layer); select(&mut g.f.app, &[g.f.node, g.yn]); let before = g.f.app.document.snapshot(); assert!(!g.f.app.document.can_undo()); let from = node_point(&g.f.app, g.f.node); let mut old_canvas = CanvasState::default(); let old = begin(&mut g.f.app, &mut old_canvas, from); assert!(g.f.app.allocation_drag.active.is_some()); let location = g.f.app.document.compiled().index.declarations[&g.f.work].clone(); let original = &before.bundle.files[&location.path]; let quoted_name = format!("\"{}\"", g.f.app.project().elements[&g.f.work].name); assert_eq!(original.matches(quoted_name.as_str()).count(), 1); let edited = original.replace(quoted_name.as_str(), "\"Changed through Text\""); let mut expected = before.decoded.project.clone(); expected.elements.get_mut(&g.f.work).unwrap().name = "Changed through Text".into(); let mut expected_bundle = before.bundle.clone(); expected_bundle .files .insert(location.path.clone(), edited.clone()); let _ = g.f.app.update(Message::Page(Page::Text)); assert!(g.f.app.allocation_drag.active.is_none()); let _ = g.f.app.update(Message::SourceFile(location.path)); let _ = g.f.app .update(Message::SourceEdit(text_editor::Action::SelectAll)); let _ = g.f.app .update(Message::SourceEdit(text_editor::Action::Edit( text_editor::Edit::Paste(Arc::new(edited.clone())), ))); assert!(g.f.app.source_dirty()); assert_eq!(g.f.app.source_editor.text(), edited); assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot())); let _ = g.f.app.update(Message::ApplySource); assert!(!g.f.app.source_dirty(), "{}", g.f.app.status); assert_eq!(g.f.app.project(), &expected); assert_eq!(g.f.app.document.bundle(), &expected_bundle); let _ = g.f.app.update(Message::Page(Page::Diagram)); assert_eq!(g.f.app.active_diagram, Some(g.f.diagram)); select(&mut g.f.app, &[g.f.node, g.yn]); let from = node_point(&g.f.app, g.f.node); let to = from + Vector::new(1400., 0.); let mut current_canvas = CanvasState::default(); let current = begin(&mut g.f.app, &mut current_canvas, from); assert_ne!(current.token, old.token); assert_ne!(current.stamp.context, old.stamp.context); move_pointer(&mut g.f.app, &mut current_canvas, to); let preview = g.f.app .allocation_drag .preview .as_ref() .expect(&g.f.app.status) .clone(); let Candidate::Assignment(plan) = &preview.candidate else { panic!("the current gesture must be a real group assignment"); }; assert_eq!(plan.changed_count(), 2); let accepted = g.f.app.document.snapshot(); let revision = g.f.app.document.revision(); let held = selection(&g.f.app); let selected = g.f.app.multiselect_snapshot().unwrap(); let workspace = g.f.app.source_workspace.bundle(); let editor = g.f.app.source_editor.text(); let camera = ( g.f.app.zoom, g.f.app.pan, g.f.app.fit_to_view, g.f.app.canvas_size, ); let epochs = ( g.f.app.canvas_epoch, g.f.app.camera_epoch, g.f.app.allocation_drag.epoch, ); let draft = g.f.app .draft .as_ref() .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()); let form = g.f.app.behavior.recovery_snapshot(); let status = g.f.app.status.clone(); let pending_reveal = g.f.app.pending_diagram_reveal.clone(); let arrange = g.f.app.allocation_drag.arrange_next; // An idle obsolete press intentionally reports a stale-view explanation. // With a newer real gesture active, both old packets must be fully inert. for event in [ Event::Press(old.clone()), Event::Release(old.stamp, old.token, Some(to)), ] { let _ = g.f.app.update(Message::WorkDrag(event)); assert!(Arc::ptr_eq(&accepted, &g.f.app.document.snapshot())); assert_eq!(g.f.app.project(), &expected); assert_eq!(g.f.app.document.bundle(), &expected_bundle); assert_eq!(g.f.app.document.revision(), revision); assert_eq!(selection(&g.f.app), held); assert_eq!(g.f.app.multiselect_snapshot(), Some(selected.clone())); assert!(Arc::ptr_eq( &g.f.app.allocation_drag.active.as_ref().unwrap().press, ¤t )); assert!(Arc::ptr_eq( g.f.app.allocation_drag.preview.as_ref().unwrap(), &preview )); assert_eq!(g.f.app.source_workspace.bundle(), workspace); assert_eq!(g.f.app.source_editor.text(), editor); assert_eq!( ( g.f.app.zoom, g.f.app.pan, g.f.app.fit_to_view, g.f.app.canvas_size ), camera ); assert_eq!( ( g.f.app.canvas_epoch, g.f.app.camera_epoch, g.f.app.allocation_drag.epoch ), epochs ); assert_eq!( g.f.app .draft .as_ref() .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()), draft ); assert_eq!(g.f.app.behavior.recovery_snapshot(), form); assert_eq!(g.f.app.pending_diagram_reveal, pending_reveal); assert_eq!(g.f.app.allocation_drag.arrange_next, arrange); assert_eq!(g.f.app.status, status); assert!(!g.f.app.source_dirty()); assert!(g.f.app.document.can_undo()); assert!(!g.f.app.document.can_redo()); } // The current gesture remains only a preview. History contains solely // the accepted text rename, never either stale assignment packet. let _ = g.f.app.update(Message::Undo); assert_eq!(g.f.app.project(), &before.decoded.project); assert_eq!(g.f.app.document.bundle(), &before.bundle); assert!(!g.f.app.document.can_undo()); assert!(g.f.app.document.can_redo()); let _ = g.f.app.update(Message::Redo); assert_eq!(g.f.app.project(), &expected); assert_eq!(g.f.app.document.bundle(), &expected_bundle); assert!(g.f.app.document.can_undo()); assert!(!g.f.app.document.can_redo()); } } #[test] fn group_assignment_split_rebuild_preserves_primary_and_commits_one_complete_candidate() { for layer in [Layer::Oa, Layer::Sa] { for anchor_is_primary in [false, true] { let mut g = group(layer); select(&mut g.f.app, &[g.f.node, g.yn]); let held = selection(&g.f.app); let before = g.f.app.project().clone(); let source = g.f.app.document.bundle().clone(); let anchor = if anchor_is_primary { g.yn } else { g.f.node }; let from = node_point(&g.f.app, anchor); let to = from + Vector::new(1400., 0.); let mut state = CanvasState::default(); let packet = begin(&mut g.f.app, &mut state, from); assert_eq!(packet.target, SelectionTarget::Placement(anchor)); assert_eq!( selection(&g.f.app), held, "member press must not inspect/collapse" ); assert_eq!(g.f.app.document.bundle(), &source); move_pointer(&mut g.f.app, &mut state, to); let preview = g.f.app .allocation_drag .preview .as_ref() .expect(&g.f.app.status); let Candidate::Assignment(plan) = &preview.candidate else { panic!("group must use combined assignment"); }; assert_eq!(plan.changed_count(), 2); assert_eq!(plan.already_correct_count(), 0); assert_eq!(selection(&g.f.app), held); assert_eq!(g.f.app.project(), &before, "preview is immutable"); release(&mut g.f.app, &mut state, to); let mut expected = before.clone(); for (relation, work) in [(g.ax, g.f.work), (g.by, g.y)] { expected.relations.get_mut(&relation).unwrap().body = RelationBody::Allocation { source: g.c, target: work, }; } let d = expected.diagrams.get_mut(&g.f.diagram).unwrap(); d.nodes.get_mut(&g.f.node).unwrap().position = xy(1540., 200.); d.nodes.get_mut(&g.yn).unwrap().position = xy(1540., 600.); assert_eq!(selection(&g.f.app), held); exact_history(&mut g.f.app, &before, &source, &expected); } } } #[test] fn group_arrange_work_and_performer_anchors_preserve_every_responsibility() { for layer in [Layer::Oa, Layer::Sa] { for performer_anchor in [false, true] { let mut g = group(layer); select(&mut g.f.app, &[g.an, g.yn]); let held = selection(&g.f.app); let before = g.f.app.project().clone(); let source = g.f.app.document.bundle().clone(); let _ = g.f.app.update(Message::WorkDrag(Event::ArrangeNext)); assert!(g.f.app.allocation_drag.arrange_next); let from = node_point(&g.f.app, if performer_anchor { g.an } else { g.yn }); let to = from + Vector::new(400., 0.); let mut state = CanvasState::default(); let packet = begin(&mut g.f.app, &mut state, from); assert_eq!(packet.mode, Mode::Arrange); assert!(!g.f.app.allocation_drag.arrange_next); assert_eq!(selection(&g.f.app), held); move_pointer(&mut g.f.app, &mut state, to); assert!( matches!( g.f.app .allocation_drag .preview .as_ref() .map(|p| &p.candidate), Some(Candidate::Arrange(_)) ), "{}", g.f.app.status ); release(&mut g.f.app, &mut state, to); let mut expected = before.clone(); let d = expected.diagrams.get_mut(&g.f.diagram).unwrap(); d.nodes.get_mut(&g.an).unwrap().position = xy(500., 100.); d.nodes.get_mut(&g.f.node).unwrap().position = xy(540., 200.); d.nodes.get_mut(&g.yn).unwrap().position = xy(540., 600.); assert_eq!(selection(&g.f.app), held); exact_history(&mut g.f.app, &before, &source, &expected); } } } #[test] fn unsupported_group_click_collapses_but_threshold_crossing_never_becomes_click() { for performer_anchor in [false, true] { for distance in [0., 3., 3.25] { let mut g = group(Layer::Oa); select(&mut g.f.app, &[g.an, g.f.node]); let held = selection(&g.f.app); let before = g.f.app.document.snapshot(); let anchor = if performer_anchor { g.an } else { g.f.node }; let from = node_point(&g.f.app, anchor); let mut state = CanvasState::default(); begin(&mut g.f.app, &mut state, from); assert_eq!(selection(&g.f.app), held); move_pointer(&mut g.f.app, &mut state, from + Vector::new(distance, 0.)); if distance > 3. { assert!( g.f.app .allocation_drag .feedback .as_ref() .is_some_and(|f| f.refused) ); assert!(g.f.app.allocation_drag.preview.is_none()); } move_pointer(&mut g.f.app, &mut state, from); release(&mut g.f.app, &mut state, from); assert!(g.f.app.allocation_drag.active.is_none()); assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot())); if distance > 3. { assert_eq!( selection(&g.f.app), held, "crossed-and-returned refusal must retain group" ); } else { let s = g.f.app.diagram_selection.as_ref().unwrap(); assert_eq!(s.targets(), &[SelectionTarget::Placement(anchor)]); assert_eq!(s.primary(), Some(&SelectionTarget::Placement(anchor))); } } } } #[test] fn allocation_bare_moves_cannot_bypass_armed_active_or_retired_lifetime() { for (stage, singleton) in (0..3).flat_map(|stage| [(stage, false), (stage, true)]) { let mut g = group(Layer::Oa); let ids = if singleton { vec![g.an] } else { vec![g.an, g.cn] }; select(&mut g.f.app, &ids); let snapshot = g.f.app.multiselect_snapshot().unwrap(); let held = selection(&g.f.app); let before = g.f.app.document.snapshot(); let _ = g.f.app.update(Message::WorkDrag(Event::ArrangeNext)); let mut state = CanvasState::default(); let from = node_point(&g.f.app, g.an); if stage > 0 { let press = begin(&mut g.f.app, &mut state, from); if stage == 2 { let _ = g.f.app .update(Message::WorkDrag(Event::Cancel(press.token))); } } let epoch = g.f.app.allocation_drag.epoch; let armed = g.f.app.allocation_drag.arrange_next; let token = g.f.app .allocation_drag .active .as_ref() .map(|a| a.press.token); let _ = g.f.app.update(Message::MoveNode(g.an, xy(500., 100.))); let _ = g.f.app .update(Message::DiagramSelection(multiselect::Event::Move( snapshot, xy(400., 0.), ))); assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot())); assert_eq!(selection(&g.f.app), held); assert_eq!(g.f.app.allocation_drag.epoch, epoch); assert_eq!(g.f.app.allocation_drag.arrange_next, armed); assert_eq!( g.f.app .allocation_drag .active .as_ref() .map(|a| a.press.token), token ); } // A fresh ordinary performer gesture still uses the existing layout policy. // C's right edge leaves the viewport: explicit Arrange's header gate must not // accidentally become a new restriction on ordinary performer movement. let mut g = group(Layer::Oa); select(&mut g.f.app, &[g.an, g.cn]); let held = selection(&g.f.app); let before = g.f.app.project().clone(); let source = g.f.app.document.bundle().clone(); let _ = g.f.app.update(Message::WorkDrag(Event::ArrangeNext)); let from = node_point(&g.f.app, g.an); let mut state = CanvasState::default(); let old = begin(&mut g.f.app, &mut state, from); let _ = g.f.app.update(Message::WorkDrag(Event::Cancel(old.token))); let mut state = CanvasState::default(); let fresh = begin(&mut g.f.app, &mut state, from); assert_eq!(fresh.mode, Mode::Assign); let to = from + Vector::new(300., 0.); move_pointer(&mut g.f.app, &mut state, to); assert!( matches!( g.f.app .allocation_drag .preview .as_ref() .map(|p| &p.candidate), Some(Candidate::Performers(_)) ), "{}", g.f.app.status ); release(&mut g.f.app, &mut state, to); let mut expected = before.clone(); let d = expected.diagrams.get_mut(&g.f.diagram).unwrap(); d.nodes.get_mut(&g.an).unwrap().position = xy(400., 100.); d.nodes.get_mut(&g.f.node).unwrap().position = xy(440., 200.); d.nodes.get_mut(&g.cn).unwrap().position = xy(1800., 100.); assert_eq!(selection(&g.f.app), held); exact_history(&mut g.f.app, &before, &source, &expected); } #[test] fn idle_group_press_rejects_selection_generation_aba_with_unchanged_other_stamps() { for membership_aba in [false, true] { let mut g = group(Layer::Oa); select(&mut g.f.app, &[g.f.node, g.yn]); let old = press(&g.f); let before = g.f.app.document.snapshot(); let held = selection(&g.f.app); let epoch = g.f.app.allocation_drag.epoch; // Deliberately isolate selection generation from App message retirement. // Final selection/primary/context/Document values are unchanged; this // exercises the snapshot boundary, not an ordinary user-input sequence. let s = g.f.app.diagram_selection.as_mut().unwrap(); if membership_aba { assert!(s.remove(&SelectionTarget::Placement(g.yn))); assert!(s.add(SelectionTarget::Placement(g.yn), |_| true)); } else { assert!(s.set_primary(&SelectionTarget::Placement(g.f.node))); assert!(s.set_primary(&SelectionTarget::Placement(g.yn))); } assert_eq!(selection(&g.f.app), held); assert_eq!(g.f.app.allocation_drag.epoch, epoch); assert_eq!(g.f.app.route_context(), Some(old.stamp.context)); let _ = g.f.app.update(Message::WorkDrag(Event::Press(old.clone()))); assert!( g.f.app.allocation_drag.active.is_none(), "generation-only stale selection must not acquire input" ); let _ = g.f.app.update(Message::WorkDrag(Event::Release( old.stamp, old.token, Some(start(&g.f)), ))); assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot())); assert_eq!(selection(&g.f.app), held); } } #[test] fn group_selection_aba_and_old_press_cannot_retire_a_new_token() { let mut g = group(Layer::Oa); select(&mut g.f.app, &[g.f.node, g.yn]); let old = press(&g.f); choose( &mut g.f.app, SelectionTarget::Placement(g.f.node), Choice::Inspect, ); choose( &mut g.f.app, SelectionTarget::Placement(g.yn), Choice::Inspect, ); let before = g.f.app.document.snapshot(); let from = node_point(&g.f.app, g.f.node); let mut state = CanvasState::default(); let fresh = begin(&mut g.f.app, &mut state, from); let held = selection(&g.f.app); let epoch = g.f.app.allocation_drag.epoch; let _ = g.f.app.update(Message::WorkDrag(Event::Press(old.clone()))); let _ = g.f.app.update(Message::WorkDrag(Event::Release( old.stamp, old.token, Some(from + Vector::new(1400., 0.)), ))); assert_eq!( g.f.app .allocation_drag .active .as_ref() .map(|a| a.press.token), Some(fresh.token) ); assert_eq!(g.f.app.allocation_drag.epoch, epoch); assert_eq!(selection(&g.f.app), held); assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot())); release(&mut g.f.app, &mut state, from); } fn unassign(g: &mut Group, inside_c: bool) { let mut project = g.f.app.project().clone(); project.relations.remove(&g.ax); project.relations.remove(&g.by); if inside_c { let d = project.diagrams.get_mut(&g.f.diagram).unwrap(); // These unassigned cards overlap an unrelated empty performer at the // same depth. Paint C first so the intended work press is visible and // independent of random UUID ordering; no semantic containment exists. let mut target = d.nodes.remove(&g.cn).unwrap(); g.cn = Id::from_u128(1); target.id = g.cn; assert!(d.nodes.insert(g.cn, target).is_none()); d.nodes.get_mut(&g.f.node).unwrap().position = xy(1540., 200.); d.nodes.get_mut(&g.yn).unwrap().position = xy(1540., 600.); } g.f.app = App::with_project(project); g.f.app.active_diagram = Some(g.f.diagram); g.f.app.zoom = 1.; g.f.app.pan = Vector::ZERO; g.f.app.fit_to_view = false; g.f.app.reconcile_multiselect(); } fn c_allocations(expected: &mut Project, g: &Group, ids: &BTreeMap) { for (work, name, alias) in [ (g.f.work, "C performs Work", "c_performs_work"), (g.y, "C performs Y", "c_performs_y"), ] { expected.relations.insert( ids[&work], Relation { id: ids[&work], alias: alias.into(), name: name.into(), description: String::new(), body: RelationBody::Allocation { source: g.c, target: work, }, }, ); } } #[test] fn group_out_and_back_assigns_at_zero_delta_then_true_noop_preserves_redo() { for layer in [Layer::Oa, Layer::Sa] { let mut g = group(layer); unassign(&mut g, true); select(&mut g.f.app, &[g.f.node, g.yn]); let held = selection(&g.f.app); let before = g.f.app.project().clone(); let source = g.f.app.document.bundle().clone(); let from = node_point(&g.f.app, g.f.node); let mut state = CanvasState::default(); let press = begin(&mut g.f.app, &mut state, from); assert_eq!(press.target, SelectionTarget::Placement(g.f.node)); assert_eq!(selection(&g.f.app), held); move_pointer(&mut g.f.app, &mut state, from + Vector::new(4., 0.)); let preview = g.f.app .allocation_drag .preview .as_ref() .expect(&g.f.app.status); assert_eq!(preview.placement.delta, xy(0., 0.)); let Candidate::Assignment(candidate) = &preview.candidate else { panic!("exact work group must preview assignment"); }; assert_eq!(candidate.destination(), plan::Destination::Performer(g.c)); assert_eq!(candidate.changed_count(), 2); let ids: BTreeMap<_, _> = preview .project() .relations .values() .filter(|r| r.kind() == RelationKind::Allocation) .map(|r| (r.target(), r.id)) .collect(); assert_eq!(ids.len(), 2); move_pointer(&mut g.f.app, &mut state, from); release(&mut g.f.app, &mut state, from); let mut expected = before.clone(); c_allocations(&mut expected, &g, &ids); assert_eq!(selection(&g.f.app), held); exact_history(&mut g.f.app, &before, &source, &expected); g.f.app .document .execute(Command::Describe(g.y, "Future rationale".into())) .unwrap(); g.f.app.sync(); let future = g.f.app.document.bundle().clone(); let _ = g.f.app.update(Message::Undo); assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo()); let snapshot = g.f.app.document.snapshot(); let revision = g.f.app.document.revision(); let held = selection(&g.f.app); let from = node_point(&g.f.app, g.f.node); let mut state = CanvasState::default(); begin(&mut g.f.app, &mut state, from); move_pointer(&mut g.f.app, &mut state, from + Vector::new(4., 0.)); move_pointer(&mut g.f.app, &mut state, from); release(&mut g.f.app, &mut state, from); assert!( Arc::ptr_eq(&snapshot, &g.f.app.document.snapshot()), "{}", g.f.app.status ); assert_eq!(g.f.app.document.revision(), revision); assert_eq!(selection(&g.f.app), held); assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo()); let _ = g.f.app.update(Message::Redo); assert_eq!(g.f.app.document.bundle(), &future); } } #[test] fn group_source_refusal_retains_primary_source_and_both_history_paths() { let mut g = group(Layer::Oa); unassign(&mut g, false); let source = g.f.app.document.bundle().clone(); let mut limits = oa_core::source_project::ProjectLimits::default(); limits.model.max_source_bytes = source.files.values().map(String::len).sum::() + 128; g.f.app.document = crate::document::Document::from_bundle(source, limits).unwrap(); g.f.app .document .execute(Command::Describe(g.f.work, "Reviewed".into())) .unwrap(); g.f.app .document .execute(Command::Rename(g.y, "Future Y".into())) .unwrap(); let future = g.f.app.document.bundle().clone(); assert!(g.f.app.document.undo()); g.f.app.sync(); g.f.app.reconcile_multiselect(); select(&mut g.f.app, &[g.f.node, g.yn]); let held = selection(&g.f.app); let before = g.f.app.document.snapshot(); let source = g.f.app.document.bundle().clone(); let revision = g.f.app.document.revision(); assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo()); let from = node_point(&g.f.app, g.f.node); let to = from + Vector::new(1400., 0.); let mut state = CanvasState::default(); begin(&mut g.f.app, &mut state, from); move_pointer(&mut g.f.app, &mut state, to); assert!( g.f.app.allocation_drag.preview.is_some(), "geometry and semantics pass before source refusal: {}", g.f.app.status ); release(&mut g.f.app, &mut state, to); assert!(g.f.app.status.contains("retry"), "{}", g.f.app.status); assert!(g.f.app.allocation_drag.active.is_none()); assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot())); assert_eq!(g.f.app.document.bundle(), &source); assert_eq!(g.f.app.document.revision(), revision); assert_eq!(selection(&g.f.app), held); assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo()); let _ = g.f.app.update(Message::Redo); assert_eq!(g.f.app.document.bundle(), &future); let _ = g.f.app.update(Message::Undo); assert_eq!(g.f.app.document.bundle(), &source); } #[test] fn ordinary_off_grid_performer_out_and_back_is_noop_with_redo() { for singleton in [false, true] { let mut g = group(Layer::Oa); let mut project = g.f.app.project().clone(); project .diagrams .get_mut(&g.f.diagram) .unwrap() .nodes .get_mut(&g.an) .unwrap() .position = xy(103., 107.); g.f.app = App::with_project(project); g.f.app.active_diagram = Some(g.f.diagram); g.f.app.zoom = 1.; g.f.app.pan = Vector::ZERO; g.f.app.fit_to_view = false; g.f.app .document .execute(Command::Describe(g.f.work, "Reviewed".into())) .unwrap(); g.f.app .document .execute(Command::Describe(g.y, "Future".into())) .unwrap(); let future = g.f.app.document.bundle().clone(); assert!(g.f.app.document.undo()); g.f.app.sync(); g.f.app.reconcile_multiselect(); select( &mut g.f.app, &if singleton { vec![g.an] } else { vec![g.an, g.cn] }, ); let held = selection(&g.f.app); let before = g.f.app.document.snapshot(); let source = g.f.app.document.bundle().clone(); let revision = g.f.app.document.revision(); let from = node_point(&g.f.app, g.an); let mut state = CanvasState::default(); begin(&mut g.f.app, &mut state, from); move_pointer(&mut g.f.app, &mut state, from + Vector::new(20., 0.)); assert!( g.f.app.allocation_drag.preview.is_some(), "{}", g.f.app.status ); move_pointer(&mut g.f.app, &mut state, from); assert_eq!( g.f.app .allocation_drag .preview .as_ref() .unwrap() .placement .delta, xy(0., 0.) ); release(&mut g.f.app, &mut state, from); assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot())); assert_eq!(g.f.app.document.bundle(), &source); assert_eq!(g.f.app.document.revision(), revision); assert_eq!(selection(&g.f.app), held); assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo()); let _ = g.f.app.update(Message::Redo); assert_eq!(g.f.app.document.bundle(), &future); } }