From 542b10ac587b932c0404580a98159bd497775ef0 Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Thu, 24 Sep 2026 01:17:40 -0700 Subject: [PATCH] Test Process and Allocation stale gestures after real Text Apply --- .../src/app/allocation_drag_tests/group.rs | 159 ++++++++++++++++++ .../src/app/process_multiselect/tests.rs | 145 ++++++++++++++++ 2 files changed, 304 insertions(+) diff --git a/crates/oa-desktop/src/app/allocation_drag_tests/group.rs b/crates/oa-desktop/src/app/allocation_drag_tests/group.rs index 6a60d986..eb894abb 100644 --- a/crates/oa-desktop/src/app/allocation_drag_tests/group.rs +++ b/crates/oa-desktop/src/app/allocation_drag_tests/group.rs @@ -190,6 +190,165 @@ fn exact_history( 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] { diff --git a/crates/oa-desktop/src/app/process_multiselect/tests.rs b/crates/oa-desktop/src/app/process_multiselect/tests.rs index f29afa20..dec14985 100644 --- a/crates/oa-desktop/src/app/process_multiselect/tests.rs +++ b/crates/oa-desktop/src/app/process_multiselect/tests.rs @@ -144,6 +144,151 @@ fn marker(app: &mut App, owner: Id, occurrence: Id) { assert!(app.diagram_focus(Surface::Process(owner)).is_some()); } +#[test] +fn completed_text_apply_rejects_captured_process_reorder_without_retiring_current_state() { + for layer in [Layer::Oa, Layer::Sa] { + let mut f = fixture(layer); + pair(&mut f); + let stale = f.app.multiselect_snapshot().unwrap(); + let before = f.app.document.snapshot(); + assert!(!f.app.document.can_undo()); + let element = f.nodes[0]; + let location = f.app.document.compiled().index.declarations[&element].clone(); + let original = &before.bundle.files[&location.path]; + let quoted_name = format!("\"{}\"", f.app.project().elements[&element].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(&element).unwrap().name = "Changed through Text".into(); + let mut expected_bundle = before.bundle.clone(); + expected_bundle + .files + .insert(location.path.clone(), edited.clone()); + + let _ = f.app.update(Message::Page(Page::Text)); + let _ = f.app.update(Message::SourceFile(location.path)); + let _ = f + .app + .update(Message::SourceEdit(text_editor::Action::SelectAll)); + let _ = f.app.update(Message::SourceEdit(text_editor::Action::Edit( + text_editor::Edit::Paste(Arc::new(edited.clone())), + ))); + assert!(f.app.source_dirty()); + assert_eq!(f.app.source_editor.text(), edited); + assert!(Arc::ptr_eq(&before, &f.app.document.snapshot())); + let _ = f.app.update(Message::ApplySource); + assert!(!f.app.source_dirty(), "{}", f.app.status); + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.document.bundle(), &expected_bundle); + source_matches(&f.app); + + let _ = f.app.update(Message::Page(Page::Behavior)); + assert_eq!(f.app.topology_context().unwrap().owner, f.owner); + pair(&mut f); + let current = f.app.multiselect_snapshot().unwrap(); + assert_eq!(current.targets(), stale.targets()); + assert_eq!(current.primary(), stale.primary()); + assert_ne!(current.context(), stale.context()); + let destination = Destination::After(f.nodes[2]); + assert!( + matches!( + Prepared::new(f.app.project(), f.owner, current.targets()) + .unwrap() + .plan(destination) + .unwrap(), + Plan::Change(_) + ), + "the replay must be a real reorder, not a no-op" + ); + assert!( + f.app + .multiselect_envelope(&Event::ProcessReorder(current.clone(), destination)) + ); + let accepted = f.app.document.snapshot(); + let revision = f.app.document.revision(); + let workspace = f.app.source_workspace.bundle(); + let editor = f.app.source_editor.text(); + let selected = ( + f.app.selected, + f.app.selected_node, + f.app.selected_bend, + f.app.selected_attachment, + ); + let camera = (f.app.zoom, f.app.pan, f.app.fit_to_view, f.app.canvas_size); + let process_camera = f.app.behavior.topology.camera_boundary(); + let epochs = ( + f.app.canvas_epoch, + f.app.camera_epoch, + f.app.allocation_drag.epoch, + ); + let draft = f + .app + .draft + .as_ref() + .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()); + let form = f.app.behavior.recovery_snapshot(); + let status = f.app.status.clone(); + let pending_reveal = f.app.pending_diagram_reveal.clone(); + + send(&mut f.app, Event::ProcessReorder(stale, destination)); + assert!(Arc::ptr_eq(&accepted, &f.app.document.snapshot())); + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.document.bundle(), &expected_bundle); + assert_eq!(f.app.document.revision(), revision); + assert_eq!(f.app.source_workspace.bundle(), workspace); + assert_eq!(f.app.source_editor.text(), editor); + assert_eq!(f.app.multiselect_snapshot(), Some(current)); + assert_eq!( + ( + f.app.selected, + f.app.selected_node, + f.app.selected_bend, + f.app.selected_attachment + ), + selected + ); + assert_eq!( + (f.app.zoom, f.app.pan, f.app.fit_to_view, f.app.canvas_size), + camera + ); + assert!(f.app.behavior.topology.camera_boundary() == process_camera); + assert_eq!( + ( + f.app.canvas_epoch, + f.app.camera_epoch, + f.app.allocation_drag.epoch + ), + epochs + ); + assert_eq!( + f.app + .draft + .as_ref() + .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()), + draft + ); + assert_eq!(f.app.behavior.recovery_snapshot(), form); + assert_eq!(f.app.pending_diagram_reveal, pending_reveal); + assert_eq!(f.app.status, status); + assert!(!f.app.source_dirty()); + assert!(f.app.document.can_undo()); + assert!(!f.app.document.can_redo()); + + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + assert!(f.app.document.can_redo()); + source_matches(&f.app); + let _ = f.app.update(Message::Redo); + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.document.bundle(), &expected_bundle); + assert!(f.app.document.can_undo()); + assert!(!f.app.document.can_redo()); + source_matches(&f.app); + } +} + #[test] fn process_group_reverse_click_order_changes_only_owner_steps_and_undoes_once() { for layer in [Layer::Oa, Layer::Sa] {