Reject stale Scenario actions before shared reveal boundaries
This commit is contained in:
@@ -1386,9 +1386,20 @@ impl App {
|
|||||||
|| current.visit != captured.visit
|
|| current.visit != captured.visit
|
||||||
|| (!matches!(event, behavior::scenario::CanvasEvent::Restored(..))
|
|| (!matches!(event, behavior::scenario::CanvasEvent::Restored(..))
|
||||||
&& current.revision != captured.revision)
|
&& current.revision != captured.revision)
|
||||||
|
|| (!matches!(
|
||||||
|
event,
|
||||||
|
behavior::scenario::CanvasEvent::FormShown(..)
|
||||||
|
| behavior::scenario::CanvasEvent::Viewport(..)
|
||||||
|
| behavior::scenario::CanvasEvent::Restored(..)
|
||||||
|
| behavior::scenario::CanvasEvent::Scrolled(..)
|
||||||
|
| behavior::scenario::CanvasEvent::Camera(..)
|
||||||
|
) && current.epoch != captured.epoch)
|
||||||
}) {
|
}) {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
// Passive layout/form reports retain their existing epoch rules;
|
||||||
|
// camera input uses its separate lifetime below. All other actions
|
||||||
|
// must match the exact context before shared reveal reconciliation.
|
||||||
if let behavior::scenario::CanvasEvent::Camera(_, lifetime, _) = event
|
if let behavior::scenario::CanvasEvent::Camera(_, lifetime, _) = event
|
||||||
&& *lifetime != self.camera_epoch
|
&& *lifetime != self.camera_epoch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ fn exercise(via_guide: bool) {
|
|||||||
}
|
}
|
||||||
assert_destination(&app, target);
|
assert_destination(&app, target);
|
||||||
let camera = app.camera_boundary();
|
let camera = app.camera_boundary();
|
||||||
// The prior saved canvas cannot regain focus after either owner route.
|
// The prior saved canvas cannot open its outline after either owner route.
|
||||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Shelf(
|
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Shelf(
|
||||||
held,
|
held,
|
||||||
crate::behavior::scenario::Shelf::Selection,
|
crate::behavior::scenario::Shelf::Selection,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ fn stale_text_and_scenario_messages_do_not_retire_a_new_saved_spacing_gesture()
|
|||||||
context,
|
context,
|
||||||
Size::new(900., 600.),
|
Size::new(900., 600.),
|
||||||
)));
|
)));
|
||||||
|
let context = app.scenario_context().unwrap();
|
||||||
let (event, start, _) = begin(&mut app, Target::Lane(lane));
|
let (event, start, _) = begin(&mut app, Target::Lane(lane));
|
||||||
move_to(&mut app, event, start + Vector::new(24., 0.));
|
move_to(&mut app, event, start + Vector::new(24., 0.));
|
||||||
let accepted = app.document.snapshot();
|
let accepted = app.document.snapshot();
|
||||||
@@ -314,6 +315,63 @@ fn stale_finish_cannot_remove_new_same_view_deferred_reveal() {
|
|||||||
assert!(!app.document.can_undo());
|
assert!(!app.document.can_undo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_active_actions_cannot_remove_new_same_visit_deferred_reveal() {
|
||||||
|
let (mut app, lane, message) = setup();
|
||||||
|
let old = app.scenario_context().unwrap();
|
||||||
|
let behavior::scenario_layout::Representation::Saved { diagram } = old.representation else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
let accepted = app.document.snapshot();
|
||||||
|
let source = app.source_workspace.bundle();
|
||||||
|
let _ = app.update(Message::RevealInDiagram(diagram, message));
|
||||||
|
let current = app.scenario_context().unwrap();
|
||||||
|
assert_eq!(current.visit, old.visit);
|
||||||
|
assert_eq!(current.representation, old.representation);
|
||||||
|
assert_eq!(current.revision, old.revision);
|
||||||
|
assert_ne!(current.epoch, old.epoch);
|
||||||
|
let pending = app.pending_diagram_reveal.clone();
|
||||||
|
assert!(pending.is_some());
|
||||||
|
let focus = app.diagram_navigation_focus.as_ref().unwrap();
|
||||||
|
let focus = (focus.context, focus.usage.clone(), focus.selected);
|
||||||
|
let camera = app.camera_boundary();
|
||||||
|
for event in [
|
||||||
|
CanvasEvent::Representation(old, old.representation),
|
||||||
|
CanvasEvent::Shelf(old, behavior::scenario::Shelf::Selection),
|
||||||
|
CanvasEvent::Select(old, lane),
|
||||||
|
CanvasEvent::Zoom(old, Some(1.)),
|
||||||
|
] {
|
||||||
|
let _ = app.update(Message::ScenarioCanvas(event));
|
||||||
|
assert_eq!(app.scenario_context(), Some(current));
|
||||||
|
assert_eq!(app.pending_diagram_reveal, pending);
|
||||||
|
assert_eq!(app.pending_scenario_reveal_visit, Some(current.visit));
|
||||||
|
let actual = app.diagram_navigation_focus.as_ref().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(actual.context, actual.usage.clone(), actual.selected),
|
||||||
|
focus
|
||||||
|
);
|
||||||
|
assert!(app.camera_boundary() == camera);
|
||||||
|
assert_eq!(app.selected, message);
|
||||||
|
assert_eq!(app.document.bundle(), &accepted.bundle);
|
||||||
|
assert_eq!(app.project(), &accepted.decoded.project);
|
||||||
|
assert_eq!(app.source_workspace.bundle(), source);
|
||||||
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||||
|
}
|
||||||
|
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport(
|
||||||
|
current,
|
||||||
|
Size::new(800., 500.),
|
||||||
|
)));
|
||||||
|
assert!(app.pending_diagram_reveal.is_none());
|
||||||
|
assert!(app.pending_scenario_reveal_visit.is_none());
|
||||||
|
assert!(
|
||||||
|
app.diagram_focus(crate::diagram_usage::Surface::Saved(diagram))
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
assert_eq!(app.selected, message);
|
||||||
|
assert_eq!(app.document.bundle(), &accepted.bundle);
|
||||||
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn modal_open_cancels_preview_and_captured_begin_stays_retired_after_close() {
|
fn modal_open_cancels_preview_and_captured_begin_stays_retired_after_close() {
|
||||||
let (mut app, lane, _) = setup();
|
let (mut app, lane, _) = setup();
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ representation/visit checks also run before those effects. Scope input-owner
|
|||||||
equality now includes Scenario representation and visit. Current source Tab and
|
equality now includes Scenario representation and visit. Current source Tab and
|
||||||
selection behavior are retained, as are passive WorkDrag reveal exceptions.
|
selection behavior are retained, as are passive WorkDrag reveal exceptions.
|
||||||
|
|
||||||
|
Independent review found a same-visit, newer-epoch stale action could retire a
|
||||||
|
newer deferred reveal before its late rejection. Active Scenario actions now
|
||||||
|
require the exact context before shared boundaries. FormShown, Viewport,
|
||||||
|
Restored and Scrolled retain their existing passive admission, while Camera
|
||||||
|
retains its separate lifetime rule. A regression captures arrangement/outline/
|
||||||
|
selection/zoom actions, starts a same-view reveal, verifies all stale actions
|
||||||
|
preserve its full receipt/focus/source/history, and completes the live viewport.
|
||||||
|
|
||||||
Root intentionally removed the obsolete panel-focus mode and Canvas button.
|
Root intentionally removed the obsolete panel-focus mode and Canvas button.
|
||||||
This integration retains that always-active canvas policy. Saved Scenario
|
This integration retains that always-active canvas policy. Saved Scenario
|
||||||
Details opens the current inspector and dismisses its outline without replacing
|
Details opens the current inspector and dismisses its outline without replacing
|
||||||
|
|||||||
Reference in New Issue
Block a user