628 lines
25 KiB
Rust
628 lines
25 KiB
Rust
//! Delete through the actual App widget tree, ordinary focus, and source history.
|
|
use super::*;
|
|
use crate::behavior::{
|
|
canvas_workspace::tests::{Mounted, focused, key},
|
|
scenario, topology,
|
|
};
|
|
use iced::{Event as Input, Point, Size, Vector, keyboard, mouse};
|
|
|
|
fn app(p: Project, owner: Id) -> App {
|
|
let mut app = App::with_project(p);
|
|
let _ = app.update(Message::Select(owner));
|
|
let _ = app.update(Message::Page(Page::Behavior));
|
|
app.window_size = Size::new(1900., 1200.);
|
|
app
|
|
}
|
|
fn point(mounted: &mut Mounted, app: &App, id: Id) -> Point {
|
|
if let Some(context) = app.topology_context() {
|
|
let bounds = mounted.geometry(app).bounds("process-viewport");
|
|
let local = app
|
|
.behavior
|
|
.topology
|
|
.removal_test_point(app, context.owner, id, None);
|
|
bounds.position() + Vector::new(local.x, local.y)
|
|
} else {
|
|
let context = app.scenario_context().unwrap();
|
|
let bounds = mounted.geometry(app).bounds("scenario-viewport");
|
|
let local = scenario::removal_test_point(app, context.scenario, id);
|
|
bounds.position() + Vector::new(local.x, local.y)
|
|
}
|
|
}
|
|
async fn select(mounted: &mut Mounted, app: &mut App, id: Id) {
|
|
let target = point(mounted, app, id);
|
|
mounted.click(app, target).await;
|
|
assert_eq!(app.selected, id, "real pointer selects intended occurrence");
|
|
}
|
|
fn remove_key(shift: bool) -> Input {
|
|
let mut event = key(keyboard::key::Named::Delete);
|
|
if shift && let Input::Keyboard(keyboard::Event::KeyPressed { modifiers, .. }) = &mut event {
|
|
*modifiers = keyboard::Modifiers::SHIFT;
|
|
}
|
|
event
|
|
}
|
|
fn removal(message: &Message) -> bool {
|
|
matches!(
|
|
message,
|
|
Message::TopologyCanvas(topology::CanvasEvent::Remove(..))
|
|
| Message::ScenarioCanvas(scenario::CanvasEvent::Remove(..))
|
|
)
|
|
}
|
|
async fn delete(mounted: &mut Mounted, app: &mut App, shift: bool) -> u64 {
|
|
let messages = mounted
|
|
.batch(app, &[remove_key(shift)], mouse::Cursor::Unavailable)
|
|
.await;
|
|
assert!(
|
|
messages.iter().any(removal),
|
|
"focused viewport emits removal: {messages:?}"
|
|
);
|
|
let target = app.delete_target.as_ref().expect("captured confirmation");
|
|
assert_eq!(target.snapshot.request.object, app.selected);
|
|
assert_eq!(target.placement, None);
|
|
assert_eq!(target.snapshot.request.placement, None);
|
|
target.snapshot.token
|
|
}
|
|
async fn send(mounted: &mut Mounted, app: &mut App, message: Message) {
|
|
let task = app.update(message);
|
|
mounted.perform(app, task).await;
|
|
}
|
|
fn consistent(app: &App) {
|
|
assert_eq!(
|
|
source_project::compile_bundle(app.document.bundle(), LIMITS)
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
*app.project()
|
|
);
|
|
assert_eq!(
|
|
source_workspace::reachable(&app.source_workspace.bundle()).unwrap(),
|
|
*app.document.bundle()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_process_delete_occurrence_or_link_preserves_reusable_work_and_source_history() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
for link in [false, true] {
|
|
let f = topology::direct_tests::fixture(layer);
|
|
let target = if link { f.link } else { f.nodes[0] };
|
|
let mut app = app(f.p.clone(), f.owner);
|
|
let mut mounted = Mounted::new(&mut app).await;
|
|
let original = app.document.bundle().clone();
|
|
select(&mut mounted, &mut app, target).await;
|
|
// No extra redraw between release and first Delete.
|
|
let token = delete(&mut mounted, &mut app, link).await;
|
|
assert_eq!(
|
|
app.delete_target.as_ref().unwrap().behavior_owner,
|
|
Some(f.owner)
|
|
);
|
|
assert_eq!(app.document.bundle(), &original);
|
|
send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await;
|
|
assert!(!app.project().contains(target));
|
|
assert!(f.work.iter().all(|id| app.project().contains(*id)));
|
|
assert!(app.project().contains(f.exchange));
|
|
assert!(app.project().contains(f.nodes[1]));
|
|
assert_eq!(app.project().contains(f.nodes[0]), link);
|
|
assert!(!app.project().contains(f.link));
|
|
consistent(&app);
|
|
let removed = app.document.bundle().clone();
|
|
send(&mut mounted, &mut app, Message::Undo).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
consistent(&app);
|
|
send(&mut mounted, &mut app, Message::Redo).await;
|
|
assert_eq!(app.document.bundle(), &removed);
|
|
consistent(&app);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_scenario_delete_message_or_participant_preserves_reusable_targets() {
|
|
for participant in [false, true] {
|
|
let f = scenario::direct_tests::fixture();
|
|
let target = if participant { f.sender } else { f.call };
|
|
let mut app = app(f.project.clone(), f.owner);
|
|
let mut mounted = Mounted::new(&mut app).await;
|
|
let original = app.document.bundle().clone();
|
|
select(&mut mounted, &mut app, target).await;
|
|
let token = delete(&mut mounted, &mut app, participant).await;
|
|
send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await;
|
|
assert!(!app.project().contains(target));
|
|
assert!(app.project().contains(f.exchange));
|
|
assert!(app.project().contains(f.receiver));
|
|
assert!(app.project().contains(f.reply));
|
|
for element in f
|
|
.project
|
|
.elements
|
|
.values()
|
|
.filter(|element| element.parent != Some(f.owner))
|
|
{
|
|
assert!(
|
|
app.project().contains(element.id),
|
|
"referenced definitions survive"
|
|
);
|
|
}
|
|
if participant {
|
|
assert!(app.project().contains(f.call));
|
|
for element in app.project().elements.values() {
|
|
if let Some(SharedBody::Occurrence {
|
|
body:
|
|
OccurrenceBody::Exchange {
|
|
sender, receiver, ..
|
|
},
|
|
..
|
|
}) = element.shared()
|
|
{
|
|
assert_ne!(*sender, Some(target));
|
|
assert_ne!(*receiver, Some(target));
|
|
}
|
|
}
|
|
}
|
|
consistent(&app);
|
|
send(&mut mounted, &mut app, Message::Undo).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
consistent(&app);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_scenario_use_delete_keeps_definition_and_second_use_and_rejects_owner_replay() {
|
|
let (p, owners, uses) = scenario::direct_tests::navigation_fixture();
|
|
let mut app = app(p, owners[0]);
|
|
let mut mounted = Mounted::new(&mut app).await;
|
|
let original = app.document.bundle().clone();
|
|
select(&mut mounted, &mut app, uses[0]).await;
|
|
let stale = delete(&mut mounted, &mut app, false).await;
|
|
send(&mut mounted, &mut app, Message::CancelDelete).await;
|
|
send(&mut mounted, &mut app, Message::ConfirmDelete(stale)).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
select(&mut mounted, &mut app, uses[0]).await;
|
|
let token = delete(&mut mounted, &mut app, true).await;
|
|
send(&mut mounted, &mut app, Message::ConfirmDelete(stale)).await;
|
|
assert!(app.delete_pending, "old token does not cancel new dialog");
|
|
// Keep selection and epoch deliberately unchanged: owner is an independent witness.
|
|
let captured = app.scenario_context().unwrap();
|
|
assert_eq!(captured.scenario, owners[0]);
|
|
assert_eq!(
|
|
app.delete_target.as_ref().unwrap().behavior_owner,
|
|
Some(owners[0])
|
|
);
|
|
app.behavior.active_owner = Some(owners[1]);
|
|
app.reconcile_scenario_workspace();
|
|
let other = app.scenario_context().unwrap();
|
|
assert_eq!(other.scenario, owners[1]);
|
|
assert_eq!(
|
|
other.representation,
|
|
crate::behavior::scenario_layout::Representation::Derived { owner: owners[1] }
|
|
);
|
|
assert_eq!(other.epoch, captured.epoch);
|
|
assert_eq!(app.selected, uses[0]);
|
|
assert!(
|
|
app.delete_target.as_ref().unwrap().snapshot.valid(&app),
|
|
"the independent owner guard must reject an otherwise current token"
|
|
);
|
|
send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
assert!(!app.delete_pending && app.delete_target.is_none());
|
|
assert!(!app.document.can_undo() && !app.document.can_redo());
|
|
app.behavior.active_owner = Some(owners[0]);
|
|
// Restore both owner and qualified presentation before mounting pointer input.
|
|
// Passive reports from the previous owner cannot repair an inconsistent fixture.
|
|
app.reconcile_scenario_workspace();
|
|
assert_eq!(app.scenario_context().unwrap().scenario, owners[0]);
|
|
assert_eq!(app.selected, uses[0]);
|
|
mounted.settle(&mut app).await;
|
|
select(&mut mounted, &mut app, uses[0]).await;
|
|
let token = delete(&mut mounted, &mut app, false).await;
|
|
send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await;
|
|
assert!(!app.project().contains(uses[0]));
|
|
assert!(app.project().contains(uses[1]));
|
|
assert!(owners.iter().all(|id| app.project().contains(*id)));
|
|
consistent(&app);
|
|
send(&mut mounted, &mut app, Message::Undo).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_behavior_tab_and_f6_retire_same_batch_delete_and_window_focus() {
|
|
for process in [false, true] {
|
|
let (mut app, target) = if process {
|
|
let f = topology::direct_tests::fixture(Layer::Oa);
|
|
(app(f.p, f.owner), f.nodes[0])
|
|
} else {
|
|
let f = scenario::direct_tests::fixture();
|
|
(app(f.project, f.owner), f.call)
|
|
};
|
|
let mut mounted = Mounted::new(&mut app).await;
|
|
let original = app.document.bundle().clone();
|
|
for boundary in [
|
|
keyboard::key::Named::Tab,
|
|
keyboard::key::Named::F6,
|
|
keyboard::key::Named::Escape,
|
|
] {
|
|
select(&mut mounted, &mut app, target).await;
|
|
let messages = mounted
|
|
.batch(
|
|
&mut app,
|
|
&[key(boundary), remove_key(false)],
|
|
mouse::Cursor::Unavailable,
|
|
)
|
|
.await;
|
|
assert!(
|
|
!messages.iter().any(removal),
|
|
"{boundary:?} retires keyboard admission within the batch"
|
|
);
|
|
assert!(!app.delete_pending);
|
|
assert_eq!(app.document.bundle(), &original);
|
|
assert!(!focused(&mut mounted, &app, "viewport"));
|
|
if boundary == keyboard::key::Named::Tab {
|
|
assert_eq!(
|
|
mounted
|
|
.geometry(&app)
|
|
.focused
|
|
.iter()
|
|
.filter(|(_, _, value)| *value)
|
|
.count(),
|
|
1,
|
|
"Tab completes traversal to one real control"
|
|
);
|
|
let mut backwards = key(keyboard::key::Named::Tab);
|
|
if let Input::Keyboard(keyboard::Event::KeyPressed { modifiers, .. }) =
|
|
&mut backwards
|
|
{
|
|
*modifiers = keyboard::Modifiers::SHIFT;
|
|
}
|
|
mounted
|
|
.batch(&mut app, &[backwards], mouse::Cursor::Unavailable)
|
|
.await;
|
|
} else {
|
|
let messages = mounted
|
|
.batch(
|
|
&mut app,
|
|
&[key(keyboard::key::Named::F6), remove_key(false)],
|
|
mouse::Cursor::Unavailable,
|
|
)
|
|
.await;
|
|
assert!(
|
|
!messages.iter().any(removal),
|
|
"F6 entry completes after the batch, before a later Delete"
|
|
);
|
|
}
|
|
assert!(
|
|
focused(&mut mounted, &app, "viewport"),
|
|
"{boundary:?} supports keyboard-only return to the viewport"
|
|
);
|
|
delete(&mut mounted, &mut app, false).await;
|
|
send(&mut mounted, &mut app, Message::CancelDelete).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
}
|
|
select(&mut mounted, &mut app, target).await;
|
|
let messages = mounted
|
|
.batch(
|
|
&mut app,
|
|
&[
|
|
Input::Window(iced::window::Event::Unfocused),
|
|
remove_key(false),
|
|
],
|
|
mouse::Cursor::Unavailable,
|
|
)
|
|
.await;
|
|
assert!(!messages.iter().any(removal));
|
|
assert!(!app.delete_pending);
|
|
// A new actual click reacquires ownership after each boundary.
|
|
select(&mut mounted, &mut app, target).await;
|
|
delete(&mut mounted, &mut app, false).await;
|
|
send(&mut mounted, &mut app, Message::CancelDelete).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_behavior_clean_inspector_click_and_tab_own_delete_then_f6_returns_to_canvas() {
|
|
for process in [false, true] {
|
|
for tab_entry in [false, true] {
|
|
let (mut app, target) = if process {
|
|
let f = topology::direct_tests::fixture(Layer::Oa);
|
|
(app(f.p, f.owner), f.nodes[0])
|
|
} else {
|
|
let f = scenario::direct_tests::fixture();
|
|
(app(f.project, f.owner), f.call)
|
|
};
|
|
let mut mounted = Mounted::new(&mut app).await;
|
|
select(&mut mounted, &mut app, target).await;
|
|
let original = app.document.bundle().clone();
|
|
let original_name = app.draft.as_ref().unwrap().inputs["/name"].raw.clone();
|
|
// The first text input in the right-hand Inspector is the name.
|
|
// Verify its actual binding below, rather than seeding focus state.
|
|
let field = mounted
|
|
.geometry(&app)
|
|
.text_inputs
|
|
.into_iter()
|
|
.filter(|bounds| bounds.x > app.window_size.width - 320.)
|
|
.min_by(|a, b| a.y.total_cmp(&b.y))
|
|
.expect("visible Inspector name input");
|
|
if tab_entry {
|
|
// Canvas Tab stays within canvas controls. Escape then ordinary
|
|
// Tab follows the existing Inspector entry policy.
|
|
mounted
|
|
.batch(
|
|
&mut app,
|
|
&[key(keyboard::key::Named::Escape)],
|
|
mouse::Cursor::Unavailable,
|
|
)
|
|
.await;
|
|
mounted
|
|
.batch(
|
|
&mut app,
|
|
&[key(keyboard::key::Named::Tab)],
|
|
mouse::Cursor::Unavailable,
|
|
)
|
|
.await;
|
|
} else {
|
|
mounted.click(&mut app, field.center()).await;
|
|
}
|
|
assert!(
|
|
mounted
|
|
.geometry(&app)
|
|
.focused
|
|
.iter()
|
|
.any(|(_, bounds, value)| *bounds == field && *value),
|
|
"actual {} focuses the Inspector name",
|
|
if tab_entry { "Tab" } else { "click" }
|
|
);
|
|
assert!(!focused(&mut mounted, &app, "viewport"));
|
|
assert!(!app.draft_dirty());
|
|
let messages = mounted
|
|
.batch(
|
|
&mut app,
|
|
&[key(keyboard::key::Named::End), remove_key(true)],
|
|
mouse::Cursor::Unavailable,
|
|
)
|
|
.await;
|
|
assert!(messages.iter().any(|message| matches!(
|
|
message,
|
|
Message::Field(path, value) if path == "/name" && *value == original_name
|
|
)));
|
|
assert!(!messages.iter().any(removal));
|
|
assert!(!app.delete_pending);
|
|
assert!(!app.draft_dirty(), "Delete at end keeps the field clean");
|
|
assert_eq!(app.document.bundle(), &original);
|
|
mounted
|
|
.batch(
|
|
&mut app,
|
|
&[key(keyboard::key::Named::F6)],
|
|
mouse::Cursor::Unavailable,
|
|
)
|
|
.await;
|
|
assert!(focused(&mut mounted, &app, "viewport"));
|
|
assert_eq!(
|
|
mounted
|
|
.geometry(&app)
|
|
.focused
|
|
.iter()
|
|
.filter(|(_, _, value)| *value)
|
|
.count(),
|
|
1,
|
|
"F6 releases actual Inspector focus"
|
|
);
|
|
delete(&mut mounted, &mut app, false).await;
|
|
send(&mut mounted, &mut app, Message::CancelDelete).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_behavior_clean_text_focus_and_modifiers_cannot_delete_selected_occurrence() {
|
|
use iced::advanced::widget::{Operation, operation};
|
|
struct FocusText(iced::Rectangle);
|
|
impl Operation for FocusText {
|
|
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
|
|
visit(self);
|
|
}
|
|
fn focusable(
|
|
&mut self,
|
|
_: Option<&iced::advanced::widget::Id>,
|
|
bounds: iced::Rectangle,
|
|
state: &mut dyn operation::Focusable,
|
|
) {
|
|
if bounds == self.0 {
|
|
state.focus();
|
|
} else {
|
|
state.unfocus();
|
|
}
|
|
}
|
|
}
|
|
for process in [false, true] {
|
|
let (mut app, target) = if process {
|
|
let f = topology::direct_tests::fixture(Layer::Oa);
|
|
(app(f.p, f.owner), f.nodes[0])
|
|
} else {
|
|
let f = scenario::direct_tests::fixture();
|
|
(app(f.project, f.owner), f.call)
|
|
};
|
|
let mut mounted = Mounted::new(&mut app).await;
|
|
select(&mut mounted, &mut app, target).await;
|
|
let original = app.document.bundle().clone();
|
|
for modifiers in [
|
|
keyboard::Modifiers::CTRL,
|
|
keyboard::Modifiers::ALT,
|
|
keyboard::Modifiers::LOGO,
|
|
] {
|
|
let mut event = remove_key(false);
|
|
if let Input::Keyboard(keyboard::Event::KeyPressed {
|
|
modifiers: value, ..
|
|
}) = &mut event
|
|
{
|
|
*value = modifiers;
|
|
}
|
|
let messages = mounted
|
|
.batch(&mut app, &[event], mouse::Cursor::Unavailable)
|
|
.await;
|
|
assert!(!messages.iter().any(removal));
|
|
}
|
|
let field = mounted
|
|
.geometry(&app)
|
|
.text_inputs
|
|
.into_iter()
|
|
.find(|bounds| bounds.width > 40.)
|
|
.expect("real text field");
|
|
mounted.operate(&app, &mut FocusText(field));
|
|
assert!(!app.draft_dirty());
|
|
let messages = mounted
|
|
.batch(&mut app, &[remove_key(true)], mouse::Cursor::Unavailable)
|
|
.await;
|
|
assert!(!messages.iter().any(removal));
|
|
assert!(!app.delete_pending);
|
|
assert_eq!(app.document.bundle(), &original);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_scenario_fragment_and_branch_delete_only_owned_occurrences() {
|
|
for whole_fragment in [false, true] {
|
|
let (f, branch, anchor, state, reused, remote) =
|
|
scenario::direct_tests::placement_fixture();
|
|
let fragment = f.project.elements[&branch].parent.unwrap();
|
|
let target = if whole_fragment { fragment } else { branch };
|
|
let mut app = app(f.project.clone(), f.owner);
|
|
let mut mounted = Mounted::new(&mut app).await;
|
|
let original = app.document.bundle().clone();
|
|
select(&mut mounted, &mut app, target).await;
|
|
let token = delete(&mut mounted, &mut app, false).await;
|
|
send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await;
|
|
assert!(!app.project().contains(target));
|
|
assert!(!app.project().contains(anchor));
|
|
assert!(!app.project().contains(branch));
|
|
assert_eq!(app.project().contains(fragment), !whole_fragment);
|
|
for id in [
|
|
f.call, f.reply, f.sender, f.receiver, f.exchange, state, reused, remote,
|
|
] {
|
|
assert!(app.project().contains(id));
|
|
}
|
|
consistent(&app);
|
|
send(&mut mounted, &mut app, Message::Undo).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_qualified_process_child_delete_refuses_outer_reference_and_shared_definition() {
|
|
let f = topology::direct_tests::fixture(Layer::Oa);
|
|
let mut p = f.p;
|
|
let caller = p
|
|
.add_element(Kind::Process, Layer::Oa, "Caller", None)
|
|
.unwrap();
|
|
let references = ["First use", "Second use"].map(|name| {
|
|
let mut element = p
|
|
.make_element(Kind::Occurrence, Layer::Oa, name, Some(caller))
|
|
.unwrap();
|
|
*element.shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::SequenceReference {
|
|
sequence: Some(f.owner),
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
let id = element.id;
|
|
p = execute_batch(&p, &[Command::CreateElement(element)]).unwrap();
|
|
id
|
|
});
|
|
let mut app = app(p, caller);
|
|
let mut mounted = Mounted::new(&mut app).await;
|
|
let original = app.document.bundle().clone();
|
|
let endpoint = OccurrenceEndpoint {
|
|
references: vec![references[0]],
|
|
occurrence: f.nodes[0],
|
|
};
|
|
let bounds = mounted.geometry(&app).bounds("process-viewport");
|
|
let local =
|
|
app.behavior
|
|
.topology
|
|
.removal_test_point(&app, caller, references[0], Some(&endpoint));
|
|
let messages = mounted
|
|
.click(&mut app, bounds.position() + Vector::new(local.x, local.y))
|
|
.await;
|
|
assert!(messages.iter().any(|message| matches!(
|
|
message,
|
|
Message::DiagramSelection(multiselect::Event::Choose(
|
|
_, crate::diagram_usage::Target::ProcessOccurrence(actual), multiselect::Choice::Replace
|
|
)) if actual == &endpoint
|
|
)));
|
|
assert_eq!(app.selected, f.nodes[0]);
|
|
assert!(app.draft.is_none(), "Qualified inspection stays read-only");
|
|
assert_eq!(
|
|
app.behavior.topology.selection_address().unwrap().endpoint,
|
|
endpoint
|
|
);
|
|
assert!(
|
|
app.behavior
|
|
.topology
|
|
.has_qualified_selection(app.project(), caller)
|
|
);
|
|
mounted
|
|
.batch(&mut app, &[remove_key(false)], mouse::Cursor::Unavailable)
|
|
.await;
|
|
assert!(!app.delete_pending);
|
|
assert_eq!(app.document.bundle(), &original);
|
|
assert!(app.status.contains("Open the referenced definition"));
|
|
select(&mut mounted, &mut app, references[0]).await;
|
|
assert!(
|
|
!app.behavior
|
|
.topology
|
|
.has_qualified_selection(app.project(), caller)
|
|
);
|
|
let token = delete(&mut mounted, &mut app, true).await;
|
|
send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await;
|
|
assert!(!app.project().contains(references[0]));
|
|
assert!(app.project().contains(references[1]));
|
|
assert!(app.project().contains(f.owner));
|
|
assert!(f.nodes.iter().all(|id| app.project().contains(*id)));
|
|
consistent(&app);
|
|
send(&mut mounted, &mut app, Message::Undo).await;
|
|
assert_eq!(app.document.bundle(), &original);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_behavior_pan_and_stale_removal_intent_do_not_delete() {
|
|
for process in [false, true] {
|
|
let (mut app, target) = if process {
|
|
let f = topology::direct_tests::fixture(Layer::Oa);
|
|
(app(f.p, f.owner), f.nodes[0])
|
|
} else {
|
|
let f = scenario::direct_tests::fixture();
|
|
(app(f.project, f.owner), f.call)
|
|
};
|
|
let mut mounted = Mounted::new(&mut app).await;
|
|
select(&mut mounted, &mut app, target).await;
|
|
let original = app.document.bundle().clone();
|
|
let cursor = point(&mut mounted, &app, target);
|
|
let messages = mounted
|
|
.batch(
|
|
&mut app,
|
|
&[
|
|
Input::Mouse(mouse::Event::CursorMoved { position: cursor }),
|
|
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)),
|
|
remove_key(false),
|
|
Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)),
|
|
],
|
|
mouse::Cursor::Available(cursor),
|
|
)
|
|
.await;
|
|
assert!(
|
|
!messages.iter().any(removal),
|
|
"held pan never becomes an occurrence deletion"
|
|
);
|
|
assert!(!app.delete_pending);
|
|
select(&mut mounted, &mut app, target).await;
|
|
let messages = mounted.input(&app, &[remove_key(false)], mouse::Cursor::Unavailable);
|
|
let stale = messages
|
|
.into_iter()
|
|
.find(removal)
|
|
.expect("captured live removal");
|
|
app.canvas_epoch = app.canvas_epoch.wrapping_add(1);
|
|
send(&mut mounted, &mut app, stale).await;
|
|
assert!(!app.delete_pending);
|
|
assert_eq!(app.document.bundle(), &original);
|
|
}
|
|
}
|