394 lines
15 KiB
Rust
394 lines
15 KiB
Rust
use super::*;
|
|
use behavior::scenario::{
|
|
CanvasEvent,
|
|
spacing::{
|
|
Target,
|
|
controller::tests::{fixture, packet},
|
|
},
|
|
};
|
|
use iced::{Point, Size, Vector};
|
|
|
|
fn setup() -> (App, Id, Id) {
|
|
let (document, owner, diagram, lane, message) = fixture();
|
|
let mut app = App::with_project(document.project().clone());
|
|
let _ = app.update(Message::Select(owner));
|
|
let _ = app.update(Message::Diagram(Choice {
|
|
id: diagram,
|
|
label: "Saved".into(),
|
|
}));
|
|
let c = app.scenario_context().unwrap();
|
|
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport(
|
|
c,
|
|
Size::new(900., 600.),
|
|
)));
|
|
assert!(app.spacing_enabled());
|
|
(app, lane, message)
|
|
}
|
|
|
|
#[test]
|
|
fn stale_text_and_scenario_messages_do_not_retire_a_new_saved_spacing_gesture() {
|
|
let (mut app, lane, _) = setup();
|
|
let previous = app.scenario_context().unwrap();
|
|
let diagram = match previous.representation {
|
|
behavior::scenario_layout::Representation::Saved { diagram } => diagram,
|
|
_ => unreachable!(),
|
|
};
|
|
let _ = app.update(Message::Page(Page::Text));
|
|
let old_indent = Message::SourceIndent(app.source_symbol_context(), false);
|
|
let old_drawer = Message::TextWorkspace(text_workspace::Event::ShowDrawer(
|
|
app.text_drawer_context(),
|
|
text_workspace::DrawerTab::Problems,
|
|
));
|
|
let old_command = app.text_command_message(text_workspace::Command::ShowStatus);
|
|
let _ = app.update(Message::Diagram(Choice {
|
|
id: diagram,
|
|
label: "Saved".into(),
|
|
}));
|
|
let context = app.scenario_context().unwrap();
|
|
assert_ne!(context.visit, previous.visit);
|
|
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport(
|
|
context,
|
|
Size::new(900., 600.),
|
|
)));
|
|
let context = app.scenario_context().unwrap();
|
|
let (event, start, _) = begin(&mut app, Target::Lane(lane));
|
|
move_to(&mut app, event, start + Vector::new(24., 0.));
|
|
let accepted = app.document.snapshot();
|
|
let source = app.source_workspace.bundle();
|
|
let source_version = app.source_version;
|
|
let camera = app.camera_boundary();
|
|
let selected = app.selected;
|
|
let events = [
|
|
old_indent,
|
|
old_drawer,
|
|
old_command,
|
|
Message::ScenarioCanvas(CanvasEvent::Shelf(
|
|
previous,
|
|
behavior::scenario::Shelf::Selection,
|
|
)),
|
|
Message::ScenarioCanvas(CanvasEvent::Properties(previous, lane)),
|
|
];
|
|
for rejected in events {
|
|
let _ = app.update(rejected);
|
|
assert_eq!(app.scenario_context(), Some(context));
|
|
assert_eq!(app.behavior.scenario.spacing_token(), Some(event.token()));
|
|
assert_eq!(app.behavior.scenario.spacing_stamp(), Some(event.stamp()));
|
|
assert!(app.camera_boundary() == camera);
|
|
assert_eq!(app.selected, selected);
|
|
assert_eq!(app.source_version, source_version);
|
|
assert_eq!(app.source_workspace.bundle(), source);
|
|
assert_eq!(app.document.bundle(), &accepted.bundle);
|
|
assert_eq!(app.project(), &accepted.decoded.project);
|
|
assert!(app.text_workspace.drawer().is_none());
|
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
|
}
|
|
finish(&mut app, event, start + Vector::new(24., 0.));
|
|
assert!(
|
|
app.document.can_undo(),
|
|
"the current gesture remains usable"
|
|
);
|
|
}
|
|
fn begin(app: &mut App, target: Target) -> (Event, Point, f32) {
|
|
let event = packet(
|
|
app.project(),
|
|
&app.behavior.scenario,
|
|
app.scenario_context().unwrap(),
|
|
app.camera_epoch,
|
|
target,
|
|
);
|
|
let Event::Begin {
|
|
press_screen,
|
|
scale,
|
|
..
|
|
} = event
|
|
else {
|
|
unreachable!()
|
|
};
|
|
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(event)));
|
|
assert_eq!(app.behavior.scenario.spacing_token(), Some(event.token()));
|
|
(event, press_screen, scale)
|
|
}
|
|
fn send(app: &mut App, event: Event) {
|
|
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(event)));
|
|
}
|
|
fn move_to(app: &mut App, begin: Event, screen: Point) {
|
|
send(
|
|
app,
|
|
Event::Move {
|
|
stamp: begin.stamp(),
|
|
token: begin.token(),
|
|
screen,
|
|
},
|
|
);
|
|
}
|
|
fn finish(app: &mut App, begin: Event, screen: Point) {
|
|
send(
|
|
app,
|
|
Event::Finish {
|
|
stamp: begin.stamp(),
|
|
token: begin.token(),
|
|
screen,
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn same_batch_preview_is_immutable_and_one_commit_undo_redo_has_exact_source_parity() {
|
|
let (mut app, lane, _) = setup();
|
|
let original = app.document.snapshot();
|
|
let source = app.source_workspace.bundle();
|
|
let selected = app.selected;
|
|
let epoch = app.canvas_epoch;
|
|
let (event, start, scale) = begin(&mut app, Target::Lane(lane));
|
|
move_to(&mut app, event, start + Vector::new(40., 300.));
|
|
assert_eq!(app.selected, selected);
|
|
assert_eq!(app.canvas_epoch, epoch);
|
|
assert_eq!(app.project(), &original.decoded.project);
|
|
assert_eq!(app.source_workspace.bundle(), source);
|
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
|
finish(&mut app, event, start + Vector::new(40., 300.));
|
|
assert_eq!(app.selected, lane);
|
|
assert!(app.document.can_undo() && !app.document.can_redo());
|
|
let accepted = app.document.snapshot();
|
|
assert_ne!(accepted.bundle, original.bundle);
|
|
let expected = behavior::scenario::spacing::controller::tests::expected(
|
|
&original.decoded.project,
|
|
match event.stamp().context.representation {
|
|
behavior::scenario_layout::Representation::Saved { diagram } => diagram,
|
|
_ => unreachable!(),
|
|
},
|
|
Target::Lane(lane),
|
|
40. / scale,
|
|
);
|
|
assert_eq!(app.project(), &expected);
|
|
let _ = app.update(Message::ShortcutUndo(false));
|
|
assert_eq!(app.project(), &original.decoded.project);
|
|
assert_eq!(app.document.bundle(), &original.bundle);
|
|
assert!(!app.document.can_undo() && app.document.can_redo());
|
|
let _ = app.update(Message::ShortcutUndo(true));
|
|
assert_eq!(app.project(), &accepted.decoded.project);
|
|
assert_eq!(app.document.bundle(), &accepted.bundle);
|
|
}
|
|
|
|
#[test]
|
|
fn click_selects_but_crossed_return_and_cancel_preserve_selection_camera_and_history() {
|
|
let (mut app, lane, message) = setup();
|
|
let original = app.document.snapshot();
|
|
let camera = app.behavior.scenario.camera_boundary();
|
|
let (event, start, _) = begin(&mut app, Target::Message(message));
|
|
finish(&mut app, event, start + Vector::new(30., 2.));
|
|
assert_eq!(app.selected, message);
|
|
assert!(camera == app.behavior.scenario.camera_boundary());
|
|
for cancel in [false, true] {
|
|
let (event, start, _) = begin(&mut app, Target::Lane(lane));
|
|
move_to(&mut app, event, start + Vector::new(30., 0.));
|
|
if cancel {
|
|
send(
|
|
&mut app,
|
|
Event::Cancel {
|
|
stamp: event.stamp(),
|
|
token: event.token(),
|
|
},
|
|
);
|
|
} else {
|
|
finish(&mut app, event, start);
|
|
}
|
|
assert_eq!(app.selected, message);
|
|
assert!(camera == app.behavior.scenario.camera_boundary());
|
|
assert_eq!(app.document.bundle(), &original.bundle);
|
|
assert_eq!(app.project(), &original.decoded.project);
|
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stale_finish_after_new_view_and_forged_token_are_inert_before_reveal_reconciliation() {
|
|
let (mut app, lane, _) = setup();
|
|
let (event, start, _) = begin(&mut app, Target::Lane(lane));
|
|
let held_epoch = app.canvas_epoch;
|
|
send(
|
|
&mut app,
|
|
Event::Finish {
|
|
stamp: event.stamp(),
|
|
token: Id::new_v4(),
|
|
screen: start + Vector::new(30., 0.),
|
|
},
|
|
);
|
|
assert_eq!(app.canvas_epoch, held_epoch);
|
|
assert_eq!(app.behavior.scenario.spacing_token(), Some(event.token()));
|
|
let owner = event.stamp().context.scenario;
|
|
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Representation(
|
|
event.stamp().context,
|
|
behavior::scenario_layout::Representation::Derived { owner },
|
|
)));
|
|
let current = app.scenario_context();
|
|
let epoch = app.canvas_epoch;
|
|
let original = app.document.snapshot();
|
|
finish(&mut app, event, start + Vector::new(50., 0.));
|
|
assert_eq!(app.scenario_context(), current);
|
|
assert_eq!(app.canvas_epoch, epoch);
|
|
assert!(app.behavior.scenario.spacing_token().is_none());
|
|
assert_eq!(app.document.bundle(), &original.bundle);
|
|
}
|
|
|
|
#[test]
|
|
fn source_write_failure_cannot_commit_candidate_or_materialize_camera() {
|
|
let (mut app, lane, _) = setup();
|
|
let bundle = app.document.bundle().clone();
|
|
let mut limits = oa_core::source_project::ProjectLimits::default();
|
|
limits.model.max_source_bytes = bundle.files.values().map(String::len).sum();
|
|
app.document = Document::from_bundle(bundle, limits).unwrap();
|
|
app.sync();
|
|
let original = app.document.snapshot();
|
|
let selected = app.selected;
|
|
let camera = app.behavior.scenario.camera_boundary();
|
|
let (event, start, _) = begin(&mut app, Target::Lane(lane));
|
|
move_to(&mut app, event, start + Vector::new(40., 0.));
|
|
finish(&mut app, event, start + Vector::new(40., 0.));
|
|
assert!(
|
|
app.status.starts_with("Spacing was not applied:"),
|
|
"{}",
|
|
app.status
|
|
);
|
|
assert_eq!(app.selected, selected);
|
|
assert!(camera == app.behavior.scenario.camera_boundary());
|
|
assert_eq!(app.project(), &original.decoded.project);
|
|
assert_eq!(app.document.bundle(), &original.bundle);
|
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
|
assert!(app.behavior.scenario.spacing_token().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn pending_semantic_draft_and_source_edit_cancel_and_reject_held_finish() {
|
|
for source in [false, true] {
|
|
let (mut app, lane, _) = setup();
|
|
let original = app.document.snapshot();
|
|
let selected = app.selected;
|
|
let (event, start, _) = begin(&mut app, Target::Lane(lane));
|
|
move_to(&mut app, event, start + Vector::new(30., 0.));
|
|
if source {
|
|
let _ = app.update(Message::SourceEdit(text_editor::Action::Edit(
|
|
text_editor::Edit::Insert(' '),
|
|
)));
|
|
assert!(app.source_dirty());
|
|
} else {
|
|
let _ = app.update(Message::Behavior(behavior::Event::Scenario(
|
|
behavior::scenario::Event::Start(
|
|
event.stamp().context.scenario,
|
|
behavior::scenario::Tool::Lifeline,
|
|
),
|
|
)));
|
|
assert!(app.behavior.has_pending());
|
|
}
|
|
let draft = app.source_workspace.bundle();
|
|
assert!(app.behavior.scenario.spacing_token().is_none());
|
|
finish(&mut app, event, start + Vector::new(30., 0.));
|
|
assert_eq!(app.project(), &original.decoded.project);
|
|
assert_eq!(app.document.bundle(), &original.bundle);
|
|
assert_eq!(app.source_workspace.bundle(), draft);
|
|
assert_eq!(app.selected, selected);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stale_finish_cannot_remove_new_same_view_deferred_reveal() {
|
|
let (mut app, lane, message) = setup();
|
|
let (event, start, _) = begin(&mut app, Target::Lane(lane));
|
|
let behavior::scenario_layout::Representation::Saved { diagram } =
|
|
event.stamp().context.representation
|
|
else {
|
|
unreachable!()
|
|
};
|
|
let _ = app.update(Message::RevealInDiagram(diagram, message));
|
|
assert!(app.pending_diagram_reveal.is_some());
|
|
let epoch = app.canvas_epoch;
|
|
let selected = app.selected;
|
|
let visit = app.pending_scenario_reveal_visit;
|
|
let original = app.document.snapshot();
|
|
finish(&mut app, event, start + Vector::new(50., 0.));
|
|
assert_eq!(app.canvas_epoch, epoch);
|
|
assert_eq!(app.pending_scenario_reveal_visit, visit);
|
|
assert!(app.pending_diagram_reveal.is_some());
|
|
assert_eq!(app.selected, selected);
|
|
assert_eq!(app.document.bundle(), &original.bundle);
|
|
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]
|
|
fn modal_open_cancels_preview_and_captured_begin_stays_retired_after_close() {
|
|
let (mut app, lane, _) = setup();
|
|
let original = app.document.snapshot();
|
|
let camera = app.behavior.scenario.camera_boundary();
|
|
let (event, start, _) = begin(&mut app, Target::Lane(lane));
|
|
move_to(&mut app, event, start + Vector::new(30., 0.));
|
|
let _ = app.update(Message::Delete);
|
|
assert!(app.delete_pending);
|
|
assert!(app.behavior.scenario.spacing_token().is_none());
|
|
finish(&mut app, event, start + Vector::new(30., 0.));
|
|
let _ = app.update(Message::CancelDelete);
|
|
send(&mut app, event);
|
|
assert!(app.behavior.scenario.spacing_token().is_none());
|
|
assert_eq!(app.project(), &original.decoded.project);
|
|
assert_eq!(app.document.bundle(), &original.bundle);
|
|
assert!(camera == app.behavior.scenario.camera_boundary());
|
|
assert!(!app.document.can_undo());
|
|
}
|