1249 lines
43 KiB
Rust
1249 lines
43 KiB
Rust
use super::*;
|
|
use crate::diagram::{DiagramCanvas, State as CanvasState};
|
|
use iced::{
|
|
mouse,
|
|
widget::canvas::{self, Program},
|
|
};
|
|
|
|
#[path = "allocation_drag_full_tree_tests.rs"]
|
|
mod full_tree;
|
|
|
|
#[path = "allocation_drag_tests/group.rs"]
|
|
mod group;
|
|
|
|
#[path = "allocation_drag_tests/navigation_union.rs"]
|
|
mod navigation_union;
|
|
|
|
struct Fixture {
|
|
app: App,
|
|
a: Id,
|
|
b: Id,
|
|
work: Id,
|
|
node: Id,
|
|
diagram: Id,
|
|
}
|
|
fn xy(x: f32, y: f32) -> Position {
|
|
Position { x, y }
|
|
}
|
|
fn bounds() -> Rectangle {
|
|
Rectangle::new(Point::new(20., 30.), iced::Size::new(2200., 1400.))
|
|
}
|
|
fn screen(p: Position) -> Point {
|
|
Point::new(bounds().x + p.x, bounds().y + p.y)
|
|
}
|
|
fn fixture(layer: Layer) -> Fixture {
|
|
let mut p = Project::empty();
|
|
let (performer, other, work) = if layer == Layer::Oa {
|
|
(Kind::Entity, Kind::Actor, Kind::Activity)
|
|
} else {
|
|
(Kind::System, Kind::SystemActor, Kind::Function)
|
|
};
|
|
let a = p.add_element(performer, layer, "A", None).unwrap();
|
|
let b = p.add_element(other, layer, "B", None).unwrap();
|
|
let work = p.add_element(work, layer, "Work", None).unwrap();
|
|
let diagram = p.ensure_diagram(layer);
|
|
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
|
|
p.place(diagram, a, xy(100., 100.));
|
|
p.place(diagram, b, xy(900., 100.));
|
|
let node = p.place(diagram, work, xy(100., 500.));
|
|
let mut app = App::with_project(p);
|
|
app.active_diagram = Some(diagram);
|
|
app.zoom = 1.;
|
|
app.pan = Vector::ZERO;
|
|
app.fit_to_view = false;
|
|
app.reconcile_multiselect();
|
|
Fixture {
|
|
app,
|
|
a,
|
|
b,
|
|
work,
|
|
node,
|
|
diagram,
|
|
}
|
|
}
|
|
fn canvas(app: &App) -> DiagramCanvas<'_> {
|
|
DiagramCanvas {
|
|
selection: app.diagram_selection.as_ref(),
|
|
allocation: Some(app.allocation_capability()),
|
|
project: app.project(),
|
|
view_index: Some(app.document.view_index()),
|
|
diagram: app.active_diagram.unwrap(),
|
|
selected: app.selected,
|
|
navigation_focus: None,
|
|
zoom: app.zoom,
|
|
pan: app.pan,
|
|
context: app.route_context().unwrap(),
|
|
selected_bend: None,
|
|
selected_attachment: None,
|
|
editable: !app.draft_dirty() && !app.source_dirty(),
|
|
placing: false,
|
|
connection: None,
|
|
}
|
|
}
|
|
fn pointer(
|
|
canvas: &DiagramCanvas<'_>,
|
|
state: &mut CanvasState,
|
|
event: mouse::Event,
|
|
final_cursor: Point,
|
|
) -> Option<Message> {
|
|
canvas
|
|
.update(
|
|
state,
|
|
&canvas::Event::Mouse(event),
|
|
bounds(),
|
|
mouse::Cursor::Available(final_cursor),
|
|
)
|
|
.and_then(|a| a.into_inner().0)
|
|
}
|
|
fn start(f: &Fixture) -> Point {
|
|
let p = f.app.project().diagrams[&f.diagram].nodes[&f.node].position;
|
|
screen(xy(p.x + 40., p.y + 35.))
|
|
}
|
|
fn batch(app: &App, from: Point, to: Point) -> Vec<Message> {
|
|
let canvas = canvas(app);
|
|
let mut state = CanvasState::default();
|
|
[
|
|
mouse::Event::CursorMoved { position: from },
|
|
mouse::Event::ButtonPressed(mouse::Button::Left),
|
|
mouse::Event::CursorMoved { position: to },
|
|
mouse::Event::ButtonReleased(mouse::Button::Left),
|
|
]
|
|
.into_iter()
|
|
.filter_map(|e| pointer(&canvas, &mut state, e, to))
|
|
.collect()
|
|
}
|
|
fn send(app: &mut App, events: Vec<Message>) {
|
|
for e in events {
|
|
let _ = app.update(e);
|
|
}
|
|
}
|
|
fn press(f: &Fixture) -> Arc<Press> {
|
|
batch(&f.app, start(f), start(f))
|
|
.into_iter()
|
|
.find_map(|m| match m {
|
|
Message::WorkDrag(Event::Press(p)) => Some(p),
|
|
_ => None,
|
|
})
|
|
.expect("actual clean work-body press")
|
|
}
|
|
fn admit_press(f: &mut Fixture) -> Arc<Press> {
|
|
let p = press(f);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
|
assert!(f.app.allocation_drag.active.is_some(), "{}", f.app.status);
|
|
p
|
|
}
|
|
fn singleton(candidate: &Candidate) -> &Arc<plan::SemanticPlan> {
|
|
let Candidate::Singleton(plan) = candidate else {
|
|
panic!("legacy singleton candidate")
|
|
};
|
|
plan
|
|
}
|
|
fn drag(f: &mut Fixture, to: Point) {
|
|
let events = batch(&f.app, start(f), to);
|
|
assert!(matches!(
|
|
events.first(),
|
|
Some(Message::WorkDrag(Event::Press(_)))
|
|
));
|
|
assert!(events.iter().all(|m| matches!(m, Message::WorkDrag(_))));
|
|
send(&mut f.app, events);
|
|
}
|
|
|
|
#[test]
|
|
fn mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut f = fixture(layer);
|
|
let original = f.app.project().clone();
|
|
let original_source = f.app.document.bundle().clone();
|
|
drag(&mut f, screen(xy(160., 190.)));
|
|
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
|
let relation = f
|
|
.app
|
|
.project()
|
|
.relations
|
|
.values()
|
|
.find(|r| r.kind() == RelationKind::Allocation)
|
|
.unwrap()
|
|
.clone();
|
|
assert_eq!((relation.source(), relation.target()), (f.a, f.work));
|
|
assert_eq!(f.app.project().elements, original.elements);
|
|
let after_in = f.app.document.bundle().clone();
|
|
let _ = f.app.update(Message::Undo);
|
|
assert_eq!(f.app.document.bundle(), &original_source);
|
|
assert!(!f.app.document.can_undo());
|
|
let _ = f.app.update(Message::Redo);
|
|
assert_eq!(f.app.document.bundle(), &after_in);
|
|
drag(&mut f, screen(xy(960., 190.)));
|
|
let reassigned = &f.app.project().relations[&relation.id];
|
|
assert_eq!(reassigned.source(), f.b, "{}", f.app.status);
|
|
assert_eq!(reassigned.name, relation.name);
|
|
assert_eq!(reassigned.description, relation.description);
|
|
let before_out = f.app.document.bundle().clone();
|
|
drag(&mut f, screen(xy(600., 700.)));
|
|
assert!(
|
|
!f.app.project().relations.contains_key(&relation.id),
|
|
"{}",
|
|
f.app.status
|
|
);
|
|
assert_eq!(f.app.project().elements, original.elements);
|
|
let after_out = f.app.document.bundle().clone();
|
|
assert_eq!(
|
|
oa_core::source_project::compile_bundle(&after_out, Default::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
*f.app.project()
|
|
);
|
|
let _ = f.app.update(Message::Undo);
|
|
assert_eq!(f.app.document.bundle(), &before_out);
|
|
let _ = f.app.update(Message::Redo);
|
|
assert_eq!(f.app.document.bundle(), &after_out);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let before = f.app.document.snapshot();
|
|
let events = batch(&f.app, start(&f), start(&f));
|
|
send(&mut f.app, events);
|
|
assert_eq!(f.app.selected, f.work);
|
|
assert_eq!(f.app.selected_node, Some(f.node));
|
|
assert!(Arc::ptr_eq(&before, &f.app.document.snapshot()));
|
|
assert!(!f.app.document.can_undo());
|
|
assert!(f.app.allocation_drag.active.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn mounted_dirty_property_press_does_not_select_apply_or_destroy_draft() {
|
|
for same_selection in [false, true] {
|
|
let mut f = fixture(Layer::Oa);
|
|
if same_selection {
|
|
let _ = f.app.update(Message::Select(f.work));
|
|
}
|
|
let _ = f.app.update(Message::Field(
|
|
"/name".into(),
|
|
"Keep my unfinished name".into(),
|
|
));
|
|
let selection = (f.app.selected, f.app.selected_node);
|
|
let before = f.app.document.snapshot();
|
|
let dirty = f.app.draft.as_ref().unwrap().value.clone();
|
|
let input = &f.app.draft.as_ref().unwrap().inputs["/name"];
|
|
let raw = input.raw.clone();
|
|
let cursor = input.editor.cursor();
|
|
let events = batch(&f.app, start(&f), screen(xy(160., 190.)));
|
|
assert!(matches!(
|
|
events.first(),
|
|
Some(Message::WorkDrag(Event::Refused(..)))
|
|
));
|
|
assert!(!events.iter().any(|m| matches!(
|
|
m,
|
|
Message::Select(_) | Message::SelectNode(_) | Message::MoveNode(..)
|
|
)));
|
|
send(&mut f.app, events);
|
|
assert_eq!((f.app.selected, f.app.selected_node), selection);
|
|
assert_eq!(f.app.draft.as_ref().unwrap().value, dirty);
|
|
assert_eq!(f.app.draft.as_ref().unwrap().inputs["/name"].raw, raw);
|
|
assert_eq!(
|
|
f.app.draft.as_ref().unwrap().inputs["/name"]
|
|
.editor
|
|
.cursor(),
|
|
cursor
|
|
);
|
|
assert!(f.app.draft_dirty());
|
|
assert!(Arc::ptr_eq(&before, &f.app.document.snapshot()));
|
|
assert_eq!(f.app.status, FINISH_EDIT);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn source_and_modal_admission_reject_prebuilt_clean_press_without_selecting() {
|
|
for mode in 0..4 {
|
|
let mut f = fixture(Layer::Oa);
|
|
let p = press(&f);
|
|
let selection = f.app.selected;
|
|
let source = f.app.document.bundle().clone();
|
|
match mode {
|
|
0 => {
|
|
let _ = f.app.update(Message::SourceEdit(
|
|
iced::widget::text_editor::Action::Edit(
|
|
iced::widget::text_editor::Edit::Insert('x'),
|
|
),
|
|
));
|
|
}
|
|
1 => f.app.busy = true,
|
|
2 => f.app.delete_pending = true,
|
|
_ => f.app.hosted.visible = true,
|
|
}
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
|
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
|
p.stamp,
|
|
p.token,
|
|
Some(screen(xy(160., 190.))),
|
|
)));
|
|
assert_eq!(f.app.selected, selection);
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
assert!(!f.app.document.can_undo());
|
|
assert!(f.app.allocation_drag.active.is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay() {
|
|
for interruption in 0..6 {
|
|
let mut f = fixture(Layer::Oa);
|
|
let p = press(&f);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
|
let before = f.app.document.bundle().clone();
|
|
match interruption {
|
|
0 => {
|
|
f.app.delete_pending = true;
|
|
let _ = f.app.update(Message::CancelDelete);
|
|
}
|
|
1 => {
|
|
let _ = f.app.update(Message::Fit);
|
|
f.app.zoom = p.stamp.zoom;
|
|
f.app.pan = p.stamp.pan;
|
|
}
|
|
2 => {
|
|
let _ = f.app.update(Message::Undo);
|
|
}
|
|
3 => {
|
|
let _ = f
|
|
.app
|
|
.update(Message::CanvasResized(iced::Size::new(2300., 1400.)));
|
|
f.app.canvas_size = None;
|
|
}
|
|
4 => {
|
|
let _ = f.app.update(Message::WorkDrag(Event::Unfocused));
|
|
}
|
|
_ => {
|
|
let _ = f.app.update(Message::Escape);
|
|
}
|
|
}
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
|
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
|
p.stamp,
|
|
p.token,
|
|
Some(screen(xy(160., 190.))),
|
|
)));
|
|
assert_eq!(f.app.document.bundle(), &before);
|
|
assert!(!f.app.document.can_undo());
|
|
assert!(f.app.allocation_drag.active.is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let old = press(&f);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(old.clone())));
|
|
let _ = f.app.update(Message::Escape);
|
|
let new = press(&f);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(new.clone())));
|
|
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
|
old.stamp,
|
|
old.token,
|
|
Some(screen(xy(160., 190.))),
|
|
)));
|
|
assert_eq!(
|
|
f.app.allocation_drag.active.as_ref().unwrap().press.token,
|
|
new.token
|
|
);
|
|
let release = Message::WorkDrag(Event::Release(
|
|
new.stamp,
|
|
new.token,
|
|
Some(screen(xy(160., 190.))),
|
|
));
|
|
let _ = f.app.update(release.clone());
|
|
let revision = f.app.document.revision();
|
|
let _ = f.app.update(release);
|
|
assert_eq!(f.app.document.revision(), revision);
|
|
assert_eq!(revision, 1, "{}", f.app.status);
|
|
}
|
|
|
|
#[test]
|
|
fn arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let _ = f.app.update(Message::Command(Command::Rename(
|
|
f.work,
|
|
"Temporary".into(),
|
|
)));
|
|
let _ = f.app.update(Message::Undo);
|
|
let before = f.app.document.bundle().clone();
|
|
let revision = f.app.document.revision();
|
|
let _ = f.app.update(Message::WorkDrag(Event::ArrangeNext));
|
|
assert!(f.app.allocation_drag.arrange_next);
|
|
let p = press(&f);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
|
assert_eq!(p.mode, Mode::Arrange);
|
|
assert!(!f.app.allocation_drag.arrange_next);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
|
p.stamp,
|
|
p.token,
|
|
Some(p.start + Vector::new(4., 0.)),
|
|
)));
|
|
assert_eq!(f.app.document.bundle(), &before);
|
|
assert_eq!(f.app.document.revision(), revision);
|
|
assert!(f.app.document.can_redo());
|
|
assert!(f.app.project().relations.is_empty());
|
|
assert_eq!(press(&f).mode, Mode::Assign);
|
|
}
|
|
|
|
#[test]
|
|
fn outside_release_is_cancel_and_does_not_remove_an_assignment() {
|
|
let mut f = fixture(Layer::Oa);
|
|
drag(&mut f, screen(xy(160., 190.)));
|
|
let before = f.app.document.bundle().clone();
|
|
let revision = f.app.document.revision();
|
|
drag(
|
|
&mut f,
|
|
Point::new(bounds().x + bounds().width, bounds().y + 300.),
|
|
);
|
|
assert_eq!(f.app.document.bundle(), &before);
|
|
assert_eq!(f.app.document.revision(), revision);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let p = press(&f);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
|
let a = screen(xy(160., 190.));
|
|
let b = screen(xy(960., 190.));
|
|
let _ = f
|
|
.app
|
|
.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(a))));
|
|
let first = singleton(
|
|
&f.app
|
|
.allocation_drag
|
|
.preview
|
|
.as_ref()
|
|
.expect(&f.app.status)
|
|
.candidate,
|
|
)
|
|
.clone();
|
|
let stable = first
|
|
.project()
|
|
.relations
|
|
.values()
|
|
.find(|r| r.kind() == RelationKind::Allocation)
|
|
.unwrap()
|
|
.id;
|
|
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
|
p.stamp,
|
|
p.token,
|
|
Some(a + Vector::new(1., 0.)),
|
|
)));
|
|
assert!(Arc::ptr_eq(
|
|
&first,
|
|
singleton(&f.app.allocation_drag.preview.as_ref().unwrap().candidate)
|
|
));
|
|
let _ = f
|
|
.app
|
|
.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(b))));
|
|
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
|
|
assert!(!Arc::ptr_eq(&first, singleton(&preview.candidate)));
|
|
assert_eq!(preview.project().relations[&stable].source(), f.b);
|
|
assert_eq!(
|
|
preview.placement.view,
|
|
projection::project_diagram_with_index(
|
|
preview.project(),
|
|
&preview.placement.diagram,
|
|
preview.index()
|
|
)
|
|
);
|
|
assert_ne!(
|
|
preview.placement.view,
|
|
projection::project_diagram_with_index(
|
|
f.app.project(),
|
|
&preview.placement.diagram,
|
|
f.app.document.view_index()
|
|
)
|
|
);
|
|
let _ = f
|
|
.app
|
|
.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(a))));
|
|
assert!(Arc::ptr_eq(
|
|
&first,
|
|
singleton(&f.app.allocation_drag.preview.as_ref().unwrap().candidate)
|
|
));
|
|
assert_eq!(
|
|
f.app.allocation_drag.active.as_ref().unwrap().cache.len(),
|
|
2
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let p = admit_press(&mut f);
|
|
let mut frozen = f
|
|
.app
|
|
.allocation_drag
|
|
.active
|
|
.as_ref()
|
|
.unwrap()
|
|
.frozen
|
|
.as_ref()
|
|
.unwrap()
|
|
.as_ref()
|
|
.clone();
|
|
let ai = frozen
|
|
.targets
|
|
.iter()
|
|
.position(|t| t.element == f.a)
|
|
.unwrap();
|
|
let bi = frozen
|
|
.targets
|
|
.iter()
|
|
.position(|t| t.element == f.b)
|
|
.unwrap();
|
|
frozen.targets[ai].region = Rectangle::new(Point::new(100., 100.), iced::Size::new(200., 200.));
|
|
frozen.targets[bi].region = Rectangle::new(Point::new(300., 100.), iced::Size::new(200., 200.));
|
|
assert!(
|
|
matches!(frozen.resolve(p.stamp, Some(screen(xy(300., 110.))), false),
|
|
Hit::Destination(plan::Destination::Performer(id), _) if id == f.b)
|
|
);
|
|
assert!(matches!(
|
|
frozen.resolve(p.stamp, Some(screen(xy(500., 110.))), false),
|
|
Hit::Destination(plan::Destination::OutsidePerformers, None)
|
|
));
|
|
frozen.targets[bi].region = Rectangle::new(Point::new(150., 150.), iced::Size::new(50., 50.));
|
|
assert!(matches!(
|
|
frozen.resolve(p.stamp, Some(screen(xy(160., 160.))), false),
|
|
Hit::Ambiguous(_)
|
|
));
|
|
frozen.targets[bi].ancestors.push(f.a);
|
|
assert!(
|
|
matches!(frozen.resolve(p.stamp, Some(screen(xy(160., 160.))), false),
|
|
Hit::Destination(plan::Destination::Performer(id), _) if id == f.b)
|
|
);
|
|
frozen.cards.push(Rectangle::new(
|
|
Point::new(160., 160.),
|
|
iced::Size::new(20., 20.),
|
|
));
|
|
assert_eq!(
|
|
frozen.resolve(p.stamp, Some(screen(xy(160., 160.))), false),
|
|
Hit::Occluded
|
|
);
|
|
assert_eq!(frozen.resolve(p.stamp, None, false), Hit::OutsideViewport);
|
|
assert_eq!(
|
|
frozen.resolve(
|
|
p.stamp,
|
|
Some(Point::new(bounds().x + 50., bounds().y + 15.)),
|
|
false
|
|
),
|
|
Hit::Destination(plan::Destination::OutsidePerformers, None)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn release_uses_ordered_native_position_before_later_motion_in_same_batch() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let from = start(&f);
|
|
let a = screen(xy(160., 190.));
|
|
let b = screen(xy(960., 190.));
|
|
let mut state = CanvasState::default();
|
|
let canvas = canvas(&f.app);
|
|
let events: Vec<_> = [
|
|
mouse::Event::CursorMoved { position: from },
|
|
mouse::Event::ButtonPressed(mouse::Button::Left),
|
|
mouse::Event::CursorMoved { position: a },
|
|
mouse::Event::ButtonReleased(mouse::Button::Left),
|
|
mouse::Event::CursorMoved { position: b },
|
|
]
|
|
.into_iter()
|
|
.filter_map(|e| pointer(&canvas, &mut state, e, b))
|
|
.collect();
|
|
send(&mut f.app, events);
|
|
assert_eq!(
|
|
validation::effective_performer(f.app.project(), f.work),
|
|
Some(f.a),
|
|
"{}",
|
|
f.app.status
|
|
);
|
|
}
|
|
|
|
fn replace(f: &mut Fixture, project: Project) {
|
|
f.app.document = crate::document::Document::from_project(project, Default::default()).unwrap();
|
|
f.app.sync();
|
|
f.app.reconcile_multiselect();
|
|
}
|
|
|
|
#[test]
|
|
fn same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let mut project = f.app.project().clone();
|
|
let diagram = project.diagrams.get_mut(&f.diagram).unwrap();
|
|
diagram
|
|
.nodes
|
|
.values_mut()
|
|
.find(|n| n.element == f.b)
|
|
.unwrap()
|
|
.position
|
|
.x = 324.;
|
|
replace(&mut f, project);
|
|
let p = press(&f);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
|
let left = screen(xy(323.5, 140.));
|
|
let right = screen(xy(324.5, 140.));
|
|
assert_eq!(
|
|
((left.x - p.start.x + 100.) / 10.).round(),
|
|
((right.x - p.start.x + 100.) / 10.).round()
|
|
);
|
|
for at in [left, right] {
|
|
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
|
p.stamp,
|
|
p.token,
|
|
Some(at),
|
|
)));
|
|
}
|
|
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
|
assert_eq!(active.cache.len(), 2);
|
|
let a = active
|
|
.cache
|
|
.iter()
|
|
.find(|(d, _)| *d == plan::Destination::Performer(f.a))
|
|
.unwrap()
|
|
.1
|
|
.as_ref()
|
|
.unwrap();
|
|
let b = active
|
|
.cache
|
|
.iter()
|
|
.find(|(d, _)| *d == plan::Destination::Performer(f.b))
|
|
.unwrap()
|
|
.1
|
|
.as_ref()
|
|
.unwrap();
|
|
assert!(!Arc::ptr_eq(singleton(a), singleton(b)));
|
|
let stable = a
|
|
.project()
|
|
.relations
|
|
.values()
|
|
.find(|r| r.kind() == RelationKind::Allocation)
|
|
.unwrap()
|
|
.id;
|
|
assert_eq!(b.project().relations[&stable].source(), f.b);
|
|
assert!(f.app.project().relations.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let before = f.app.document.bundle().clone();
|
|
let mut limits = oa_core::source_project::ProjectLimits::default();
|
|
limits.model.max_source_bytes = before.files.values().map(String::len).sum();
|
|
f.app.document = crate::document::Document::from_bundle(before.clone(), limits).unwrap();
|
|
f.app.sync();
|
|
let snapshot = f.app.document.snapshot();
|
|
drag(&mut f, screen(xy(160., 190.)));
|
|
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
|
assert_eq!(f.app.document.bundle(), &before);
|
|
assert!(!f.app.document.can_undo());
|
|
assert!(f.app.status.contains("retry"), "{}", f.app.status);
|
|
assert!(f.app.allocation_drag.active.is_none());
|
|
f.app.document = crate::document::Document::from_bundle(before, Default::default()).unwrap();
|
|
f.app.sync();
|
|
drag(&mut f, screen(xy(160., 190.)));
|
|
assert_eq!(
|
|
validation::effective_performer(f.app.project(), f.work),
|
|
Some(f.a),
|
|
"{}",
|
|
f.app.status
|
|
);
|
|
assert_eq!(f.app.document.revision(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let mut project = f.app.project().clone();
|
|
let role = project
|
|
.add_element(Kind::Role, Layer::Oa, "Responder", None)
|
|
.unwrap();
|
|
project.relate(RelationKind::RoleAssignment, "Responsible", role, f.work);
|
|
project.relate(RelationKind::RoleRealization, "Realizes", f.a, role);
|
|
project
|
|
.diagrams
|
|
.get_mut(&f.diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&f.node)
|
|
.unwrap()
|
|
.position = xy(150., 200.);
|
|
replace(&mut f, project);
|
|
let relations = f.app.project().relations.clone();
|
|
let source = f.app.document.bundle().clone();
|
|
drag(&mut f, screen(xy(650., 700.)));
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
assert!(!f.app.document.can_undo());
|
|
assert!(
|
|
f.app.status.to_lowercase().contains("role"),
|
|
"{}",
|
|
f.app.status
|
|
);
|
|
let admitted = admit_press(&mut f);
|
|
let target = f
|
|
.app
|
|
.allocation_drag
|
|
.active
|
|
.as_ref()
|
|
.unwrap()
|
|
.frozen
|
|
.as_ref()
|
|
.unwrap()
|
|
.targets
|
|
.iter()
|
|
.find(|t| t.element == f.a)
|
|
.unwrap()
|
|
.region;
|
|
let _ = f
|
|
.app
|
|
.update(Message::WorkDrag(Event::Cancel(admitted.token)));
|
|
drag(&mut f, screen(xy(target.x + 45., target.y + 100.)));
|
|
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
|
assert_eq!(f.app.project().relations, relations);
|
|
assert!(
|
|
!f.app
|
|
.project()
|
|
.relations
|
|
.values()
|
|
.any(|r| r.kind() == RelationKind::Allocation)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let mut project = f.app.project().clone();
|
|
let child = project
|
|
.add_element(Kind::Activity, Layer::Oa, "Child", Some(f.work))
|
|
.unwrap();
|
|
let hidden = project.place(f.diagram, child, xy(150., 600.));
|
|
project
|
|
.diagrams
|
|
.get_mut(&f.diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&f.node)
|
|
.unwrap()
|
|
.collapsed = true;
|
|
replace(&mut f, project);
|
|
let before = f.app.project().diagrams[&f.diagram].nodes.clone();
|
|
drag(&mut f, screen(xy(960., 190.)));
|
|
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
|
let after = &f.app.project().diagrams[&f.diagram].nodes;
|
|
let dx = after[&f.node].position.x - before[&f.node].position.x;
|
|
let dy = after[&f.node].position.y - before[&f.node].position.y;
|
|
assert_eq!(
|
|
after[&hidden].position,
|
|
xy(
|
|
before[&hidden].position.x + dx,
|
|
before[&hidden].position.y + dy
|
|
)
|
|
);
|
|
assert!(after[&f.node].collapsed);
|
|
let assigned: Vec<_> = f
|
|
.app
|
|
.project()
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.kind() == RelationKind::Allocation)
|
|
.collect();
|
|
assert_eq!(assigned.len(), 1);
|
|
assert_eq!((assigned[0].source(), assigned[0].target()), (f.b, child));
|
|
assert_eq!(f.app.project().elements[&child].parent, Some(f.work));
|
|
}
|
|
|
|
#[test]
|
|
fn mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection() {
|
|
let mut f = fixture(Layer::Sa);
|
|
let mut project = f.app.project().clone();
|
|
let port = Id::new_v4();
|
|
project.elements.insert(
|
|
port,
|
|
make_port(
|
|
&project,
|
|
port,
|
|
f.work,
|
|
"Send",
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: vec![],
|
|
},
|
|
)
|
|
.unwrap(),
|
|
);
|
|
replace(&mut f, project);
|
|
let view = projection::project_view(f.app.project(), f.diagram);
|
|
let at = screen(
|
|
view.ports
|
|
.iter()
|
|
.find(|p| p.element == port)
|
|
.unwrap()
|
|
.position,
|
|
);
|
|
let _ = f
|
|
.app
|
|
.update(Message::Field("/name".into(), "Keep dirty name".into()));
|
|
let selection = f.app.selected;
|
|
let before = f.app.document.bundle().clone();
|
|
let events = batch(&f.app, at, at + Vector::new(30., 0.));
|
|
assert!(matches!(
|
|
events.first(),
|
|
Some(Message::WorkDrag(Event::Refused(..)))
|
|
));
|
|
send(&mut f.app, events);
|
|
assert_eq!(f.app.selected, selection);
|
|
assert_eq!(f.app.document.bundle(), &before);
|
|
assert!(f.app.draft_dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let mut project = f.app.project().clone();
|
|
project
|
|
.diagrams
|
|
.get_mut(&f.diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.values_mut()
|
|
.find(|n| n.element == f.a)
|
|
.unwrap()
|
|
.collapsed = true;
|
|
replace(&mut f, project);
|
|
let p = admit_press(&mut f);
|
|
let mut frozen = f
|
|
.app
|
|
.allocation_drag
|
|
.active
|
|
.as_ref()
|
|
.unwrap()
|
|
.frozen
|
|
.as_ref()
|
|
.unwrap()
|
|
.as_ref()
|
|
.clone();
|
|
let t = frozen.targets.iter().find(|t| t.element == f.a).unwrap();
|
|
let bottom = xy(t.region.x + 30., t.region.y + t.region.height);
|
|
assert!(t.region.height <= 62.);
|
|
assert_eq!(
|
|
frozen.resolve(p.stamp, Some(screen(bottom)), false),
|
|
Hit::Destination(plan::Destination::OutsidePerformers, None)
|
|
);
|
|
frozen.controls.push(Rectangle::new(
|
|
Point::new(500., 500.),
|
|
iced::Size::new(20., 20.),
|
|
));
|
|
assert_eq!(
|
|
frozen.resolve(p.stamp, Some(screen(xy(500., 500.))), false),
|
|
Hit::Occluded
|
|
);
|
|
frozen
|
|
.edges
|
|
.push((Point::new(600., 600.), Point::new(700., 600.)));
|
|
assert_eq!(
|
|
frozen.resolve(p.stamp, Some(screen(xy(650., 605.))), false),
|
|
Hit::Occluded
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet()
|
|
{
|
|
let mut f = fixture(Layer::Oa);
|
|
let p = press(&f);
|
|
let project = f.app.project().clone();
|
|
let _ = f.app.behavior.update(
|
|
&project,
|
|
crate::behavior::Event::Creation(crate::behavior::creation::Event::Open(Layer::Oa)),
|
|
);
|
|
assert!(f.app.behavior.has_pending());
|
|
let events = batch(&f.app, start(&f), screen(xy(160., 190.)));
|
|
assert!(matches!(
|
|
events.first(),
|
|
Some(Message::WorkDrag(Event::Refused(..)))
|
|
));
|
|
let source = f.app.document.bundle().clone();
|
|
send(&mut f.app, events);
|
|
assert!(f.app.behavior.has_pending());
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
let _ = f.app.behavior.update(
|
|
&project,
|
|
crate::behavior::Event::Creation(crate::behavior::creation::Event::Cancel),
|
|
);
|
|
replace(&mut f, project);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
|
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
|
p.stamp,
|
|
p.token,
|
|
Some(screen(xy(160., 190.))),
|
|
)));
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
assert!(!f.app.document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn actual_canvas_origin_change_cancels_held_work_before_any_legacy_release() {
|
|
let mut f = fixture(Layer::Oa);
|
|
let from = start(&f);
|
|
let mut state = CanvasState::default();
|
|
let events = {
|
|
let canvas = canvas(&f.app);
|
|
let mut events = Vec::new();
|
|
for event in [
|
|
mouse::Event::CursorMoved { position: from },
|
|
mouse::Event::ButtonPressed(mouse::Button::Left),
|
|
] {
|
|
if let Some(message) = pointer(&canvas, &mut state, event, from) {
|
|
events.push(message);
|
|
}
|
|
}
|
|
let moved_bounds = Rectangle {
|
|
x: bounds().x + 1.,
|
|
..bounds()
|
|
};
|
|
let canceled = canvas
|
|
.update(
|
|
&mut state,
|
|
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
|
moved_bounds,
|
|
mouse::Cursor::Available(screen(xy(160., 190.))),
|
|
)
|
|
.and_then(|a| a.into_inner().0)
|
|
.unwrap();
|
|
assert!(matches!(canceled, Message::WorkDrag(Event::Cancel(_))));
|
|
events.push(canceled);
|
|
events
|
|
};
|
|
let source = f.app.document.bundle().clone();
|
|
send(&mut f.app, events);
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
assert!(!f.app.document.can_undo());
|
|
assert!(f.app.allocation_drag.active.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn toolbar_status_leaves_transformed_top_edge_work_header_readable() {
|
|
for zoom in [0.75, 1.25] {
|
|
let mut f = fixture(Layer::Oa);
|
|
f.app.zoom = zoom;
|
|
f.app.pan = Vector::new(20. * zoom, 20. * zoom);
|
|
let screen = |world: Position| {
|
|
Point::new(
|
|
bounds().x + (world.x + 20.) * zoom,
|
|
bounds().y + (world.y + 20.) * zoom,
|
|
)
|
|
};
|
|
let from = screen(xy(140., 580.)); // 80-unit offset: release pointer clears the old band.
|
|
let to = screen(xy(640., 60.));
|
|
let mut events = batch(&f.app, from, to);
|
|
let release = events.pop().unwrap();
|
|
send(&mut f.app, events);
|
|
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
|
|
let header = preview
|
|
.placement
|
|
.view
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.id == f.node)
|
|
.unwrap();
|
|
assert_eq!(header.position.y, -20.);
|
|
assert!((screen(header.position).y - bounds().y).abs() < 0.01);
|
|
assert!(
|
|
f.app
|
|
.allocation_drag
|
|
.feedback
|
|
.as_ref()
|
|
.unwrap()
|
|
.targets
|
|
.is_empty()
|
|
);
|
|
let _ = f.app.update(release);
|
|
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn toolbar_destination_names_transformed_target_whose_header_is_clipped() {
|
|
for zoom in [0.75, 1.25] {
|
|
let mut f = fixture(Layer::Oa);
|
|
let mut project = f.app.project().clone();
|
|
let target = project
|
|
.diagrams
|
|
.get_mut(&f.diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.values_mut()
|
|
.find(|n| n.element == f.a)
|
|
.unwrap();
|
|
target.position.y = -41.;
|
|
target.size.height = 300.;
|
|
replace(&mut f, project);
|
|
f.app.zoom = zoom;
|
|
f.app.pan = Vector::new(20. * zoom, 20. * zoom);
|
|
let screen = |world: Position| {
|
|
Point::new(
|
|
bounds().x + (world.x + 20.) * zoom,
|
|
bounds().y + (world.y + 20.) * zoom,
|
|
)
|
|
};
|
|
let mut events = batch(&f.app, screen(xy(140., 535.)), screen(xy(200., 80.)));
|
|
let release = events.pop().unwrap();
|
|
send(&mut f.app, events);
|
|
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
|
assert!(!feedback.refused, "{}", feedback.text);
|
|
let target = &feedback.frozen.as_ref().unwrap().targets[feedback.targets[0]];
|
|
assert!(screen(xy(target.region.x, target.region.y)).y < bounds().y);
|
|
assert_eq!(feedback.destination_identity(), "A");
|
|
assert!(f.app.allocation_drag.preview.is_some());
|
|
let _ = f.app.update(release);
|
|
assert_eq!(
|
|
validation::effective_performer(f.app.project(), f.work),
|
|
Some(f.a)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history()
|
|
{
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
for (zoom, pan) in [
|
|
(1., Vector::ZERO),
|
|
(0.75, Vector::new(37., -15.)),
|
|
(1.25, Vector::new(-30., 23.)),
|
|
] {
|
|
let mut f = fixture(layer);
|
|
let mut project = f.app.project().clone();
|
|
project.elements.get_mut(&f.a).unwrap().name = "Receiver".into();
|
|
replace(&mut f, project);
|
|
// Refusals must also preserve an existing redo branch.
|
|
let _ = f.app.update(Message::Command(Command::Rename(
|
|
f.work,
|
|
"Temporary".into(),
|
|
)));
|
|
let _ = f.app.update(Message::Undo);
|
|
let pan = Vector::new(pan.x, pan.y - 100. * zoom);
|
|
f.app.zoom = zoom;
|
|
f.app.pan = pan;
|
|
f.app.fit_to_view = false;
|
|
let screen = |world: Position| {
|
|
Point::new(
|
|
bounds().x + world.x * zoom + pan.x,
|
|
bounds().y + world.y * zoom + pan.y,
|
|
)
|
|
};
|
|
let from = screen(xy(140., 535.));
|
|
// Keep the pointer inside the visible target even when the camera
|
|
// clips its top. Only the grabbed work header must be clipped.
|
|
let overlap = screen(xy(160., 110_f32.max(-pan.y / zoom + 5.)));
|
|
assert!(bounds().contains(overlap));
|
|
let clear = screen(xy(160., 190.));
|
|
let accepted = f.app.project().clone();
|
|
let source = f.app.document.bundle().clone();
|
|
let snapshot = f.app.document.snapshot();
|
|
let revision = f.app.document.revision();
|
|
let mut events = batch(&f.app, from, overlap);
|
|
let release = events.pop().unwrap();
|
|
send(&mut f.app, events);
|
|
assert!(f.app.allocation_drag.preview.is_none());
|
|
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
|
assert!(feedback.refused, "{}", feedback.text);
|
|
assert!(feedback.text.contains("Make room or adjust the view"));
|
|
assert_eq!(
|
|
feedback.frozen.as_ref().unwrap().targets[feedback.targets[0]].element,
|
|
f.a
|
|
);
|
|
assert_eq!(f.app.project(), &accepted);
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
|
assert!(!f.app.document.can_undo());
|
|
assert!(f.app.document.can_redo());
|
|
let _ = f.app.update(release);
|
|
assert!(f.app.allocation_drag.active.is_none());
|
|
assert!(f.app.allocation_drag.preview.is_none());
|
|
assert_eq!(f.app.project(), &accepted);
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
assert_eq!(f.app.document.revision(), revision);
|
|
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
|
assert!(!f.app.document.can_undo());
|
|
assert!(f.app.document.can_redo());
|
|
|
|
// In a new gesture, moving into the viewport reuses its semantic
|
|
// candidate and stable allocation identity; only geometry changes.
|
|
let mut events = batch(&f.app, from, overlap);
|
|
events.pop();
|
|
send(&mut f.app, events);
|
|
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
|
let p = active.press.clone();
|
|
let semantic = singleton(active.cache[0].1.as_ref().unwrap()).clone();
|
|
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
|
p.stamp,
|
|
p.token,
|
|
Some(clear),
|
|
)));
|
|
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
|
|
assert!(Arc::ptr_eq(&semantic, singleton(&preview.candidate)));
|
|
assert_eq!(
|
|
preview.placement.diagram.nodes[&f.node].position,
|
|
xy(120., 160.)
|
|
);
|
|
assert_eq!(
|
|
preview.placement.view,
|
|
projection::project_diagram_with_index(
|
|
semantic.project(),
|
|
&preview.placement.diagram,
|
|
semantic.index(),
|
|
)
|
|
);
|
|
let mut expected = semantic.project().clone();
|
|
expected
|
|
.diagrams
|
|
.insert(f.diagram, preview.placement.diagram.clone());
|
|
assert_eq!(f.app.project(), &accepted);
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
|
p.stamp,
|
|
p.token,
|
|
Some(clear),
|
|
)));
|
|
assert_eq!(f.app.project(), &expected, "{}", f.app.status);
|
|
assert_eq!(f.app.document.revision(), revision + 1);
|
|
assert!(f.app.document.can_undo());
|
|
assert!(!f.app.document.can_redo());
|
|
let after = f.app.document.bundle().clone();
|
|
assert_eq!(
|
|
oa_core::source_project::compile_bundle(&after, Default::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
expected
|
|
);
|
|
let _ = f.app.update(Message::Undo);
|
|
assert_eq!(f.app.project(), &accepted);
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
assert!(!f.app.document.can_undo());
|
|
let _ = f.app.update(Message::Redo);
|
|
assert_eq!(f.app.project(), &expected);
|
|
assert_eq!(f.app.document.bundle(), &after);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
for (name, collapsed) in [("Receiver", false), ("Receiving subsystem", true)] {
|
|
for (zoom, pan) in [
|
|
(1., Vector::ZERO),
|
|
(0.75, Vector::new(37., -15.)),
|
|
(1.25, Vector::new(-30., 23.)),
|
|
(1., Vector::new(0., -100.)),
|
|
] {
|
|
let mut f = fixture(layer);
|
|
let mut project = f.app.project().clone();
|
|
project.elements.get_mut(&f.a).unwrap().name = name.into();
|
|
let target = project
|
|
.diagrams
|
|
.get_mut(&f.diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.values_mut()
|
|
.find(|n| n.element == f.a)
|
|
.unwrap();
|
|
target.collapsed = collapsed;
|
|
let target_id = target.id;
|
|
replace(&mut f, project);
|
|
f.app.zoom = zoom;
|
|
f.app.pan = pan;
|
|
f.app.fit_to_view = false;
|
|
let screen = |world: Position| {
|
|
Point::new(
|
|
bounds().x + world.x * zoom + pan.x,
|
|
bounds().y + world.y * zoom + pan.y,
|
|
)
|
|
};
|
|
let accepted = f.app.project().clone();
|
|
let source = f.app.document.bundle().clone();
|
|
let snapshot = f.app.document.snapshot();
|
|
// Normal name grab (40,35), release inside the original header.
|
|
let mut events = batch(&f.app, screen(xy(140., 535.)), screen(xy(160., 140.)));
|
|
let release = events.pop().unwrap();
|
|
send(&mut f.app, events);
|
|
let feedback = f
|
|
.app
|
|
.allocation_drag
|
|
.feedback
|
|
.as_ref()
|
|
.expect(&f.app.status);
|
|
assert!(!feedback.refused, "{}", feedback.text);
|
|
assert_eq!(feedback.destination_identity(), name);
|
|
assert_eq!(
|
|
feedback.frozen.as_ref().unwrap().targets[feedback.targets[0]].placement,
|
|
target_id
|
|
);
|
|
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
|
|
assert_eq!(
|
|
preview.placement.diagram.nodes[&f.node].position,
|
|
xy(120., 110.)
|
|
);
|
|
assert!(!preview.placement.diagram.nodes[&target_id].collapsed);
|
|
assert_eq!(
|
|
preview.placement.view,
|
|
projection::project_diagram_with_index(
|
|
preview.project(),
|
|
&preview.placement.diagram,
|
|
preview.index()
|
|
)
|
|
);
|
|
let mut expected = preview.project().clone();
|
|
expected
|
|
.diagrams
|
|
.insert(f.diagram, preview.placement.diagram.clone());
|
|
assert_eq!(f.app.project(), &accepted);
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
|
assert!(!f.app.document.can_undo());
|
|
let _ = f.app.update(release);
|
|
assert_eq!(f.app.project(), &expected, "{}", f.app.status);
|
|
assert_eq!(f.app.document.revision(), 1);
|
|
assert_eq!(
|
|
validation::effective_performer(f.app.project(), f.work),
|
|
Some(f.a)
|
|
);
|
|
let after = f.app.document.bundle().clone();
|
|
let _ = f.app.update(Message::Undo);
|
|
assert_eq!(f.app.project(), &accepted);
|
|
assert_eq!(f.app.document.bundle(), &source);
|
|
assert!(!f.app.document.can_undo());
|
|
let _ = f.app.update(Message::Redo);
|
|
assert_eq!(f.app.project(), &expected);
|
|
assert_eq!(f.app.document.bundle(), &after);
|
|
assert!(!f.app.document.can_redo());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation() {
|
|
let f = fixture(Layer::Oa);
|
|
let p = press(&f);
|
|
let mut feedback = Feedback {
|
|
press: p,
|
|
frozen: None,
|
|
ordinary_performers: false,
|
|
targets: vec![],
|
|
inspection_targets: vec![],
|
|
text: "A card blocks this drop".into(),
|
|
refused: true,
|
|
};
|
|
assert_eq!(feedback.destination_identity(), "No eligible destination");
|
|
feedback.text = "Release inside the captured canvas".into();
|
|
assert_eq!(feedback.destination_identity(), "No eligible destination");
|
|
feedback.refused = false;
|
|
assert_eq!(feedback.destination_identity(), "Outside performers");
|
|
}
|