Test Process and Allocation stale gestures after real Text Apply

This commit is contained in:
2026-09-24 01:17:40 -07:00
parent 62d3ffe9cb
commit 542b10ac58
2 changed files with 304 additions and 0 deletions
@@ -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,
&current
));
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] {
@@ -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] {