Support atomic Allocation group assignment and Arrange gestures
This commit is contained in:
@@ -0,0 +1,739 @@
|
||||
use super::*;
|
||||
use crate::app::multiselect::{self, Choice};
|
||||
use crate::diagram_usage::Target as SelectionTarget;
|
||||
|
||||
struct Group {
|
||||
f: Fixture,
|
||||
y: Id,
|
||||
yn: Id,
|
||||
c: Id,
|
||||
cn: Id,
|
||||
an: Id,
|
||||
ax: Id,
|
||||
by: Id,
|
||||
}
|
||||
|
||||
fn group(layer: Layer) -> Group {
|
||||
let mut f = fixture(layer);
|
||||
let mut p = f.app.project().clone();
|
||||
let y = p
|
||||
.add_element(p.elements[&f.work].kind(), layer, "Y", None)
|
||||
.unwrap();
|
||||
let c = p
|
||||
.add_element(
|
||||
if layer == Layer::Oa {
|
||||
Kind::Actor
|
||||
} else {
|
||||
Kind::SystemActor
|
||||
},
|
||||
layer,
|
||||
"C",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let yn = p.place(f.diagram, y, xy(140., 600.));
|
||||
let cn = p.place(f.diagram, c, xy(1500., 100.));
|
||||
let d = p.diagrams.get_mut(&f.diagram).unwrap();
|
||||
d.nodes.get_mut(&f.node).unwrap().position = xy(140., 200.);
|
||||
d.nodes
|
||||
.values_mut()
|
||||
.find(|n| n.element == f.b)
|
||||
.unwrap()
|
||||
.position = xy(100., 500.);
|
||||
d.nodes.get_mut(&cn).unwrap().size = Size {
|
||||
width: 600.,
|
||||
height: 900.,
|
||||
};
|
||||
let an = d.nodes.values().find(|n| n.element == f.a).unwrap().id;
|
||||
let ax = p.relate(
|
||||
RelationKind::Allocation,
|
||||
"Reviewed X responsibility",
|
||||
f.a,
|
||||
f.work,
|
||||
);
|
||||
let by = p.relate(
|
||||
RelationKind::Allocation,
|
||||
"Reviewed Y responsibility",
|
||||
f.b,
|
||||
y,
|
||||
);
|
||||
p.relations.get_mut(&ax).unwrap().description = "Keep X provenance".into();
|
||||
p.relations.get_mut(&by).unwrap().description = "Keep Y provenance".into();
|
||||
validate_candidate(&p).unwrap();
|
||||
f.app = App::with_project(p);
|
||||
f.app.active_diagram = Some(f.diagram);
|
||||
f.app.zoom = 1.;
|
||||
f.app.pan = Vector::ZERO;
|
||||
f.app.fit_to_view = false;
|
||||
f.app.reconcile_multiselect();
|
||||
Group {
|
||||
f,
|
||||
y,
|
||||
yn,
|
||||
c,
|
||||
cn,
|
||||
an,
|
||||
ax,
|
||||
by,
|
||||
}
|
||||
}
|
||||
|
||||
fn choose(app: &mut App, target: SelectionTarget, choice: Choice) {
|
||||
let context = multiselect::context(app.route_context().unwrap());
|
||||
let _ = app.update(Message::DiagramSelection(multiselect::Event::Choose(
|
||||
context, target, choice,
|
||||
)));
|
||||
}
|
||||
|
||||
fn select(app: &mut App, ids: &[Id]) {
|
||||
for (i, id) in ids.iter().enumerate() {
|
||||
choose(
|
||||
app,
|
||||
SelectionTarget::Placement(*id),
|
||||
if i == 0 { Choice::Replace } else { Choice::Add },
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
app.diagram_selection.as_ref().unwrap().targets(),
|
||||
&ids.iter()
|
||||
.copied()
|
||||
.map(SelectionTarget::Placement)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
fn selection(
|
||||
app: &App,
|
||||
) -> (
|
||||
Vec<SelectionTarget>,
|
||||
Option<SelectionTarget>,
|
||||
Id,
|
||||
Option<Id>,
|
||||
) {
|
||||
let s = app.diagram_selection.as_ref().unwrap();
|
||||
(
|
||||
s.targets().to_vec(),
|
||||
s.primary().cloned(),
|
||||
app.selected,
|
||||
app.selected_node,
|
||||
)
|
||||
}
|
||||
|
||||
fn node_point(app: &App, node: Id) -> Point {
|
||||
let view = projection::project_view(app.project(), app.active_diagram.unwrap());
|
||||
let n = view.nodes.iter().find(|n| n.id == node).unwrap();
|
||||
screen(xy(n.position.x + 40., n.position.y + 35.))
|
||||
}
|
||||
|
||||
fn input(app: &mut App, state: &mut CanvasState, event: mouse::Event, at: Point) {
|
||||
if let Some(message) = pointer(&canvas(app), state, event, at) {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn begin(app: &mut App, state: &mut CanvasState, from: Point) -> Arc<Press> {
|
||||
input(
|
||||
app,
|
||||
state,
|
||||
mouse::Event::CursorMoved { position: from },
|
||||
from,
|
||||
);
|
||||
let message = pointer(
|
||||
&canvas(app),
|
||||
state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
from,
|
||||
)
|
||||
.unwrap();
|
||||
let Message::WorkDrag(Event::Press(packet)) = &message else {
|
||||
panic!("Allocation placement must enter its captured lifetime: {message:?}");
|
||||
};
|
||||
let packet = packet.clone();
|
||||
let _ = app.update(message);
|
||||
packet
|
||||
}
|
||||
|
||||
fn move_pointer(app: &mut App, state: &mut CanvasState, to: Point) {
|
||||
input(app, state, mouse::Event::CursorMoved { position: to }, to);
|
||||
}
|
||||
|
||||
fn release(app: &mut App, state: &mut CanvasState, to: Point) {
|
||||
input(
|
||||
app,
|
||||
state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
to,
|
||||
);
|
||||
}
|
||||
|
||||
fn exact_history(
|
||||
app: &mut App,
|
||||
before: &Project,
|
||||
source: &oa_core::source_project::SourceBundle,
|
||||
expected: &Project,
|
||||
) {
|
||||
assert_eq!(app.project(), expected);
|
||||
let after = app.document.bundle().clone();
|
||||
let reopened =
|
||||
crate::document::Document::from_bundle(after.clone(), Default::default()).unwrap();
|
||||
assert_eq!(reopened.project(), expected);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.project(), before);
|
||||
assert_eq!(app.document.bundle(), source);
|
||||
assert!(
|
||||
!app.document.can_undo(),
|
||||
"one group drag is one history step"
|
||||
);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.project(), expected);
|
||||
assert_eq!(app.document.bundle(), &after);
|
||||
assert!(!app.document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_assignment_split_rebuild_preserves_primary_and_commits_one_complete_candidate() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for anchor_is_primary in [false, true] {
|
||||
let mut g = group(layer);
|
||||
select(&mut g.f.app, &[g.f.node, g.yn]);
|
||||
let held = selection(&g.f.app);
|
||||
let before = g.f.app.project().clone();
|
||||
let source = g.f.app.document.bundle().clone();
|
||||
let anchor = if anchor_is_primary { g.yn } else { g.f.node };
|
||||
let from = node_point(&g.f.app, anchor);
|
||||
let to = from + Vector::new(1400., 0.);
|
||||
let mut state = CanvasState::default();
|
||||
let packet = begin(&mut g.f.app, &mut state, from);
|
||||
assert_eq!(packet.target, SelectionTarget::Placement(anchor));
|
||||
assert_eq!(
|
||||
selection(&g.f.app),
|
||||
held,
|
||||
"member press must not inspect/collapse"
|
||||
);
|
||||
assert_eq!(g.f.app.document.bundle(), &source);
|
||||
move_pointer(&mut g.f.app, &mut state, to);
|
||||
let preview =
|
||||
g.f.app
|
||||
.allocation_drag
|
||||
.preview
|
||||
.as_ref()
|
||||
.expect(&g.f.app.status);
|
||||
let Candidate::Assignment(plan) = &preview.candidate else {
|
||||
panic!("group must use combined assignment");
|
||||
};
|
||||
assert_eq!(plan.changed_count(), 2);
|
||||
assert_eq!(plan.already_correct_count(), 0);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
assert_eq!(g.f.app.project(), &before, "preview is immutable");
|
||||
release(&mut g.f.app, &mut state, to);
|
||||
let mut expected = before.clone();
|
||||
for (relation, work) in [(g.ax, g.f.work), (g.by, g.y)] {
|
||||
expected.relations.get_mut(&relation).unwrap().body = RelationBody::Allocation {
|
||||
source: g.c,
|
||||
target: work,
|
||||
};
|
||||
}
|
||||
let d = expected.diagrams.get_mut(&g.f.diagram).unwrap();
|
||||
d.nodes.get_mut(&g.f.node).unwrap().position = xy(1540., 200.);
|
||||
d.nodes.get_mut(&g.yn).unwrap().position = xy(1540., 600.);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
exact_history(&mut g.f.app, &before, &source, &expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_arrange_work_and_performer_anchors_preserve_every_responsibility() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for performer_anchor in [false, true] {
|
||||
let mut g = group(layer);
|
||||
select(&mut g.f.app, &[g.an, g.yn]);
|
||||
let held = selection(&g.f.app);
|
||||
let before = g.f.app.project().clone();
|
||||
let source = g.f.app.document.bundle().clone();
|
||||
let _ = g.f.app.update(Message::WorkDrag(Event::ArrangeNext));
|
||||
assert!(g.f.app.allocation_drag.arrange_next);
|
||||
let from = node_point(&g.f.app, if performer_anchor { g.an } else { g.yn });
|
||||
let to = from + Vector::new(400., 0.);
|
||||
let mut state = CanvasState::default();
|
||||
let packet = begin(&mut g.f.app, &mut state, from);
|
||||
assert_eq!(packet.mode, Mode::Arrange);
|
||||
assert!(!g.f.app.allocation_drag.arrange_next);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
move_pointer(&mut g.f.app, &mut state, to);
|
||||
assert!(
|
||||
matches!(
|
||||
g.f.app
|
||||
.allocation_drag
|
||||
.preview
|
||||
.as_ref()
|
||||
.map(|p| &p.candidate),
|
||||
Some(Candidate::Arrange(_))
|
||||
),
|
||||
"{}",
|
||||
g.f.app.status
|
||||
);
|
||||
release(&mut g.f.app, &mut state, to);
|
||||
let mut expected = before.clone();
|
||||
let d = expected.diagrams.get_mut(&g.f.diagram).unwrap();
|
||||
d.nodes.get_mut(&g.an).unwrap().position = xy(500., 100.);
|
||||
d.nodes.get_mut(&g.f.node).unwrap().position = xy(540., 200.);
|
||||
d.nodes.get_mut(&g.yn).unwrap().position = xy(540., 600.);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
exact_history(&mut g.f.app, &before, &source, &expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_group_click_collapses_but_threshold_crossing_never_becomes_click() {
|
||||
for performer_anchor in [false, true] {
|
||||
for distance in [0., 3., 3.25] {
|
||||
let mut g = group(Layer::Oa);
|
||||
select(&mut g.f.app, &[g.an, g.f.node]);
|
||||
let held = selection(&g.f.app);
|
||||
let before = g.f.app.document.snapshot();
|
||||
let anchor = if performer_anchor { g.an } else { g.f.node };
|
||||
let from = node_point(&g.f.app, anchor);
|
||||
let mut state = CanvasState::default();
|
||||
begin(&mut g.f.app, &mut state, from);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
move_pointer(&mut g.f.app, &mut state, from + Vector::new(distance, 0.));
|
||||
if distance > 3. {
|
||||
assert!(
|
||||
g.f.app
|
||||
.allocation_drag
|
||||
.feedback
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.refused)
|
||||
);
|
||||
assert!(g.f.app.allocation_drag.preview.is_none());
|
||||
}
|
||||
move_pointer(&mut g.f.app, &mut state, from);
|
||||
release(&mut g.f.app, &mut state, from);
|
||||
assert!(g.f.app.allocation_drag.active.is_none());
|
||||
assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot()));
|
||||
if distance > 3. {
|
||||
assert_eq!(
|
||||
selection(&g.f.app),
|
||||
held,
|
||||
"crossed-and-returned refusal must retain group"
|
||||
);
|
||||
} else {
|
||||
let s = g.f.app.diagram_selection.as_ref().unwrap();
|
||||
assert_eq!(s.targets(), &[SelectionTarget::Placement(anchor)]);
|
||||
assert_eq!(s.primary(), Some(&SelectionTarget::Placement(anchor)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocation_bare_moves_cannot_bypass_armed_active_or_retired_lifetime() {
|
||||
for (stage, singleton) in (0..3).flat_map(|stage| [(stage, false), (stage, true)]) {
|
||||
let mut g = group(Layer::Oa);
|
||||
let ids = if singleton {
|
||||
vec![g.an]
|
||||
} else {
|
||||
vec![g.an, g.cn]
|
||||
};
|
||||
select(&mut g.f.app, &ids);
|
||||
let snapshot = g.f.app.multiselect_snapshot().unwrap();
|
||||
let held = selection(&g.f.app);
|
||||
let before = g.f.app.document.snapshot();
|
||||
let _ = g.f.app.update(Message::WorkDrag(Event::ArrangeNext));
|
||||
let mut state = CanvasState::default();
|
||||
let from = node_point(&g.f.app, g.an);
|
||||
if stage > 0 {
|
||||
let press = begin(&mut g.f.app, &mut state, from);
|
||||
if stage == 2 {
|
||||
let _ =
|
||||
g.f.app
|
||||
.update(Message::WorkDrag(Event::Cancel(press.token)));
|
||||
}
|
||||
}
|
||||
let epoch = g.f.app.allocation_drag.epoch;
|
||||
let armed = g.f.app.allocation_drag.arrange_next;
|
||||
let token =
|
||||
g.f.app
|
||||
.allocation_drag
|
||||
.active
|
||||
.as_ref()
|
||||
.map(|a| a.press.token);
|
||||
let _ = g.f.app.update(Message::MoveNode(g.an, xy(500., 100.)));
|
||||
let _ =
|
||||
g.f.app
|
||||
.update(Message::DiagramSelection(multiselect::Event::Move(
|
||||
snapshot,
|
||||
xy(400., 0.),
|
||||
)));
|
||||
assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot()));
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
assert_eq!(g.f.app.allocation_drag.epoch, epoch);
|
||||
assert_eq!(g.f.app.allocation_drag.arrange_next, armed);
|
||||
assert_eq!(
|
||||
g.f.app
|
||||
.allocation_drag
|
||||
.active
|
||||
.as_ref()
|
||||
.map(|a| a.press.token),
|
||||
token
|
||||
);
|
||||
}
|
||||
// A fresh ordinary performer gesture still uses the existing layout policy.
|
||||
// C's right edge leaves the viewport: explicit Arrange's header gate must not
|
||||
// accidentally become a new restriction on ordinary performer movement.
|
||||
let mut g = group(Layer::Oa);
|
||||
select(&mut g.f.app, &[g.an, g.cn]);
|
||||
let held = selection(&g.f.app);
|
||||
let before = g.f.app.project().clone();
|
||||
let source = g.f.app.document.bundle().clone();
|
||||
let _ = g.f.app.update(Message::WorkDrag(Event::ArrangeNext));
|
||||
let from = node_point(&g.f.app, g.an);
|
||||
let mut state = CanvasState::default();
|
||||
let old = begin(&mut g.f.app, &mut state, from);
|
||||
let _ = g.f.app.update(Message::WorkDrag(Event::Cancel(old.token)));
|
||||
let mut state = CanvasState::default();
|
||||
let fresh = begin(&mut g.f.app, &mut state, from);
|
||||
assert_eq!(fresh.mode, Mode::Assign);
|
||||
let to = from + Vector::new(300., 0.);
|
||||
move_pointer(&mut g.f.app, &mut state, to);
|
||||
assert!(
|
||||
matches!(
|
||||
g.f.app
|
||||
.allocation_drag
|
||||
.preview
|
||||
.as_ref()
|
||||
.map(|p| &p.candidate),
|
||||
Some(Candidate::Performers(_))
|
||||
),
|
||||
"{}",
|
||||
g.f.app.status
|
||||
);
|
||||
release(&mut g.f.app, &mut state, to);
|
||||
let mut expected = before.clone();
|
||||
let d = expected.diagrams.get_mut(&g.f.diagram).unwrap();
|
||||
d.nodes.get_mut(&g.an).unwrap().position = xy(400., 100.);
|
||||
d.nodes.get_mut(&g.f.node).unwrap().position = xy(440., 200.);
|
||||
d.nodes.get_mut(&g.cn).unwrap().position = xy(1800., 100.);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
exact_history(&mut g.f.app, &before, &source, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_group_press_rejects_selection_generation_aba_with_unchanged_other_stamps() {
|
||||
for membership_aba in [false, true] {
|
||||
let mut g = group(Layer::Oa);
|
||||
select(&mut g.f.app, &[g.f.node, g.yn]);
|
||||
let old = press(&g.f);
|
||||
let before = g.f.app.document.snapshot();
|
||||
let held = selection(&g.f.app);
|
||||
let epoch = g.f.app.allocation_drag.epoch;
|
||||
// Deliberately isolate selection generation from App message retirement.
|
||||
// Final selection/primary/context/Document values are unchanged; this
|
||||
// exercises the snapshot boundary, not an ordinary user-input sequence.
|
||||
let s = g.f.app.diagram_selection.as_mut().unwrap();
|
||||
if membership_aba {
|
||||
assert!(s.remove(&SelectionTarget::Placement(g.yn)));
|
||||
assert!(s.add(SelectionTarget::Placement(g.yn), |_| true));
|
||||
} else {
|
||||
assert!(s.set_primary(&SelectionTarget::Placement(g.f.node)));
|
||||
assert!(s.set_primary(&SelectionTarget::Placement(g.yn)));
|
||||
}
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
assert_eq!(g.f.app.allocation_drag.epoch, epoch);
|
||||
assert_eq!(g.f.app.route_context(), Some(old.stamp.context));
|
||||
let _ = g.f.app.update(Message::WorkDrag(Event::Press(old.clone())));
|
||||
assert!(
|
||||
g.f.app.allocation_drag.active.is_none(),
|
||||
"generation-only stale selection must not acquire input"
|
||||
);
|
||||
let _ = g.f.app.update(Message::WorkDrag(Event::Release(
|
||||
old.stamp,
|
||||
old.token,
|
||||
Some(start(&g.f)),
|
||||
)));
|
||||
assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot()));
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_selection_aba_and_old_press_cannot_retire_a_new_token() {
|
||||
let mut g = group(Layer::Oa);
|
||||
select(&mut g.f.app, &[g.f.node, g.yn]);
|
||||
let old = press(&g.f);
|
||||
choose(
|
||||
&mut g.f.app,
|
||||
SelectionTarget::Placement(g.f.node),
|
||||
Choice::Inspect,
|
||||
);
|
||||
choose(
|
||||
&mut g.f.app,
|
||||
SelectionTarget::Placement(g.yn),
|
||||
Choice::Inspect,
|
||||
);
|
||||
let before = g.f.app.document.snapshot();
|
||||
let from = node_point(&g.f.app, g.f.node);
|
||||
let mut state = CanvasState::default();
|
||||
let fresh = begin(&mut g.f.app, &mut state, from);
|
||||
let held = selection(&g.f.app);
|
||||
let epoch = g.f.app.allocation_drag.epoch;
|
||||
let _ = g.f.app.update(Message::WorkDrag(Event::Press(old.clone())));
|
||||
let _ = g.f.app.update(Message::WorkDrag(Event::Release(
|
||||
old.stamp,
|
||||
old.token,
|
||||
Some(from + Vector::new(1400., 0.)),
|
||||
)));
|
||||
assert_eq!(
|
||||
g.f.app
|
||||
.allocation_drag
|
||||
.active
|
||||
.as_ref()
|
||||
.map(|a| a.press.token),
|
||||
Some(fresh.token)
|
||||
);
|
||||
assert_eq!(g.f.app.allocation_drag.epoch, epoch);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot()));
|
||||
release(&mut g.f.app, &mut state, from);
|
||||
}
|
||||
|
||||
fn unassign(g: &mut Group, inside_c: bool) {
|
||||
let mut project = g.f.app.project().clone();
|
||||
project.relations.remove(&g.ax);
|
||||
project.relations.remove(&g.by);
|
||||
if inside_c {
|
||||
let d = project.diagrams.get_mut(&g.f.diagram).unwrap();
|
||||
// These unassigned cards overlap an unrelated empty performer at the
|
||||
// same depth. Paint C first so the intended work press is visible and
|
||||
// independent of random UUID ordering; no semantic containment exists.
|
||||
let mut target = d.nodes.remove(&g.cn).unwrap();
|
||||
g.cn = Id::from_u128(1);
|
||||
target.id = g.cn;
|
||||
assert!(d.nodes.insert(g.cn, target).is_none());
|
||||
d.nodes.get_mut(&g.f.node).unwrap().position = xy(1540., 200.);
|
||||
d.nodes.get_mut(&g.yn).unwrap().position = xy(1540., 600.);
|
||||
}
|
||||
g.f.app = App::with_project(project);
|
||||
g.f.app.active_diagram = Some(g.f.diagram);
|
||||
g.f.app.zoom = 1.;
|
||||
g.f.app.pan = Vector::ZERO;
|
||||
g.f.app.fit_to_view = false;
|
||||
g.f.app.reconcile_multiselect();
|
||||
}
|
||||
|
||||
fn c_allocations(expected: &mut Project, g: &Group, ids: &BTreeMap<Id, Id>) {
|
||||
for (work, name, alias) in [
|
||||
(g.f.work, "C performs Work", "c_performs_work"),
|
||||
(g.y, "C performs Y", "c_performs_y"),
|
||||
] {
|
||||
expected.relations.insert(
|
||||
ids[&work],
|
||||
Relation {
|
||||
id: ids[&work],
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::Allocation {
|
||||
source: g.c,
|
||||
target: work,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_out_and_back_assigns_at_zero_delta_then_true_noop_preserves_redo() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut g = group(layer);
|
||||
unassign(&mut g, true);
|
||||
select(&mut g.f.app, &[g.f.node, g.yn]);
|
||||
let held = selection(&g.f.app);
|
||||
let before = g.f.app.project().clone();
|
||||
let source = g.f.app.document.bundle().clone();
|
||||
let from = node_point(&g.f.app, g.f.node);
|
||||
let mut state = CanvasState::default();
|
||||
let press = begin(&mut g.f.app, &mut state, from);
|
||||
assert_eq!(press.target, SelectionTarget::Placement(g.f.node));
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
move_pointer(&mut g.f.app, &mut state, from + Vector::new(4., 0.));
|
||||
let preview =
|
||||
g.f.app
|
||||
.allocation_drag
|
||||
.preview
|
||||
.as_ref()
|
||||
.expect(&g.f.app.status);
|
||||
assert_eq!(preview.placement.delta, xy(0., 0.));
|
||||
let Candidate::Assignment(candidate) = &preview.candidate else {
|
||||
panic!("exact work group must preview assignment");
|
||||
};
|
||||
assert_eq!(candidate.destination(), plan::Destination::Performer(g.c));
|
||||
assert_eq!(candidate.changed_count(), 2);
|
||||
let ids: BTreeMap<_, _> = preview
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| r.kind() == RelationKind::Allocation)
|
||||
.map(|r| (r.target(), r.id))
|
||||
.collect();
|
||||
assert_eq!(ids.len(), 2);
|
||||
move_pointer(&mut g.f.app, &mut state, from);
|
||||
release(&mut g.f.app, &mut state, from);
|
||||
let mut expected = before.clone();
|
||||
c_allocations(&mut expected, &g, &ids);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
exact_history(&mut g.f.app, &before, &source, &expected);
|
||||
g.f.app
|
||||
.document
|
||||
.execute(Command::Describe(g.y, "Future rationale".into()))
|
||||
.unwrap();
|
||||
g.f.app.sync();
|
||||
let future = g.f.app.document.bundle().clone();
|
||||
let _ = g.f.app.update(Message::Undo);
|
||||
assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo());
|
||||
let snapshot = g.f.app.document.snapshot();
|
||||
let revision = g.f.app.document.revision();
|
||||
let held = selection(&g.f.app);
|
||||
let from = node_point(&g.f.app, g.f.node);
|
||||
let mut state = CanvasState::default();
|
||||
begin(&mut g.f.app, &mut state, from);
|
||||
move_pointer(&mut g.f.app, &mut state, from + Vector::new(4., 0.));
|
||||
move_pointer(&mut g.f.app, &mut state, from);
|
||||
release(&mut g.f.app, &mut state, from);
|
||||
assert!(
|
||||
Arc::ptr_eq(&snapshot, &g.f.app.document.snapshot()),
|
||||
"{}",
|
||||
g.f.app.status
|
||||
);
|
||||
assert_eq!(g.f.app.document.revision(), revision);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo());
|
||||
let _ = g.f.app.update(Message::Redo);
|
||||
assert_eq!(g.f.app.document.bundle(), &future);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_source_refusal_retains_primary_source_and_both_history_paths() {
|
||||
let mut g = group(Layer::Oa);
|
||||
unassign(&mut g, false);
|
||||
let source = g.f.app.document.bundle().clone();
|
||||
let mut limits = oa_core::source_project::ProjectLimits::default();
|
||||
limits.model.max_source_bytes = source.files.values().map(String::len).sum::<usize>() + 128;
|
||||
g.f.app.document = crate::document::Document::from_bundle(source, limits).unwrap();
|
||||
g.f.app
|
||||
.document
|
||||
.execute(Command::Describe(g.f.work, "Reviewed".into()))
|
||||
.unwrap();
|
||||
g.f.app
|
||||
.document
|
||||
.execute(Command::Rename(g.y, "Future Y".into()))
|
||||
.unwrap();
|
||||
let future = g.f.app.document.bundle().clone();
|
||||
assert!(g.f.app.document.undo());
|
||||
g.f.app.sync();
|
||||
g.f.app.reconcile_multiselect();
|
||||
select(&mut g.f.app, &[g.f.node, g.yn]);
|
||||
let held = selection(&g.f.app);
|
||||
let before = g.f.app.document.snapshot();
|
||||
let source = g.f.app.document.bundle().clone();
|
||||
let revision = g.f.app.document.revision();
|
||||
assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo());
|
||||
let from = node_point(&g.f.app, g.f.node);
|
||||
let to = from + Vector::new(1400., 0.);
|
||||
let mut state = CanvasState::default();
|
||||
begin(&mut g.f.app, &mut state, from);
|
||||
move_pointer(&mut g.f.app, &mut state, to);
|
||||
assert!(
|
||||
g.f.app.allocation_drag.preview.is_some(),
|
||||
"geometry and semantics pass before source refusal: {}",
|
||||
g.f.app.status
|
||||
);
|
||||
release(&mut g.f.app, &mut state, to);
|
||||
assert!(g.f.app.status.contains("retry"), "{}", g.f.app.status);
|
||||
assert!(g.f.app.allocation_drag.active.is_none());
|
||||
assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot()));
|
||||
assert_eq!(g.f.app.document.bundle(), &source);
|
||||
assert_eq!(g.f.app.document.revision(), revision);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo());
|
||||
let _ = g.f.app.update(Message::Redo);
|
||||
assert_eq!(g.f.app.document.bundle(), &future);
|
||||
let _ = g.f.app.update(Message::Undo);
|
||||
assert_eq!(g.f.app.document.bundle(), &source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_off_grid_performer_out_and_back_is_noop_with_redo() {
|
||||
for singleton in [false, true] {
|
||||
let mut g = group(Layer::Oa);
|
||||
let mut project = g.f.app.project().clone();
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&g.f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&g.an)
|
||||
.unwrap()
|
||||
.position = xy(103., 107.);
|
||||
g.f.app = App::with_project(project);
|
||||
g.f.app.active_diagram = Some(g.f.diagram);
|
||||
g.f.app.zoom = 1.;
|
||||
g.f.app.pan = Vector::ZERO;
|
||||
g.f.app.fit_to_view = false;
|
||||
g.f.app
|
||||
.document
|
||||
.execute(Command::Describe(g.f.work, "Reviewed".into()))
|
||||
.unwrap();
|
||||
g.f.app
|
||||
.document
|
||||
.execute(Command::Describe(g.y, "Future".into()))
|
||||
.unwrap();
|
||||
let future = g.f.app.document.bundle().clone();
|
||||
assert!(g.f.app.document.undo());
|
||||
g.f.app.sync();
|
||||
g.f.app.reconcile_multiselect();
|
||||
select(
|
||||
&mut g.f.app,
|
||||
&if singleton {
|
||||
vec![g.an]
|
||||
} else {
|
||||
vec![g.an, g.cn]
|
||||
},
|
||||
);
|
||||
let held = selection(&g.f.app);
|
||||
let before = g.f.app.document.snapshot();
|
||||
let source = g.f.app.document.bundle().clone();
|
||||
let revision = g.f.app.document.revision();
|
||||
let from = node_point(&g.f.app, g.an);
|
||||
let mut state = CanvasState::default();
|
||||
begin(&mut g.f.app, &mut state, from);
|
||||
move_pointer(&mut g.f.app, &mut state, from + Vector::new(20., 0.));
|
||||
assert!(
|
||||
g.f.app.allocation_drag.preview.is_some(),
|
||||
"{}",
|
||||
g.f.app.status
|
||||
);
|
||||
move_pointer(&mut g.f.app, &mut state, from);
|
||||
assert_eq!(
|
||||
g.f.app
|
||||
.allocation_drag
|
||||
.preview
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.placement
|
||||
.delta,
|
||||
xy(0., 0.)
|
||||
);
|
||||
release(&mut g.f.app, &mut state, from);
|
||||
assert!(Arc::ptr_eq(&before, &g.f.app.document.snapshot()));
|
||||
assert_eq!(g.f.app.document.bundle(), &source);
|
||||
assert_eq!(g.f.app.document.revision(), revision);
|
||||
assert_eq!(selection(&g.f.app), held);
|
||||
assert!(g.f.app.document.can_undo() && g.f.app.document.can_redo());
|
||||
let _ = g.f.app.update(Message::Redo);
|
||||
assert_eq!(g.f.app.document.bundle(), &future);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user