Files
ARCADIA-Studio/crates/oa-desktop/src/app/behavior_removal_tests.rs
T

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);
}
}