Files
ARCADIA-Studio/crates/oa-desktop/src/app/state_attachment_escape_tests.rs
T
sorokin.oleg.edu 2a3b9ba822 Add exact State multiselection, atomic movement and placement Hide
Preserve the desktop-2 captured source (1286 passed, 6 existing ignored). Three menu payload blobs are staged from that verified capture without replacing working files; the strict-lint size correction follows in a separate commit. Native acceptance remains pending.
2026-09-23 03:37:01 -07:00

162 lines
5.6 KiB
Rust

//! Production App tree and its actual keyboard subscription, without a canvas click.
use super::*;
use crate::behavior::canvas_workspace::tests::{Mounted, key};
use iced::{
advanced::subscription,
futures::{StreamExt, stream, task::noop_waker_ref},
keyboard::key::Named,
mouse,
};
use std::task::{Context as PollContext, Poll};
async fn press(mounted: &mut Mounted, app: &mut App, named: Named) -> Vec<Message> {
let input = key(named);
let (mut messages, statuses) = mounted.input_with_status(
app,
std::slice::from_ref(&input),
mouse::Cursor::Unavailable,
);
let interaction = subscription::Event::Interaction {
window: iced::window::Id::unique(),
event: input,
status: statuses[0],
};
for recipe in subscription::into_recipes(app.subscription()) {
let mut output = recipe.stream(Box::pin(stream::iter([interaction.clone()])));
let mut cx = PollContext::from_waker(noop_waker_ref());
while let Poll::Ready(Some(message)) = output.poll_next_unpin(&mut cx) {
messages.push(message);
}
}
for message in messages.iter().cloned() {
let task = app.update(message);
mounted.perform(app, task).await;
}
mounted.settle(app).await;
messages
}
async fn armed() -> (App, Mounted, Id) {
let mut f = fixture();
let diagram = create_view(&mut f).diagram;
let mut project = f.app.project().clone();
let relation = project.relate(RelationKind::StateTransition, "Switch", f.a, f.b);
let mut app = App::with_project(project);
app.window_size = iced::Size::new(1800., 1100.);
send(&mut app, Message::RevealInDiagram(diagram, relation));
let mut mounted = Mounted::new(&mut app).await;
app.behavior.diagram.fit = false;
let context = app.state_context().unwrap();
event(
&mut app,
Event::ContextMenu {
context,
object: relation,
placement: None,
selection_target: None,
anchor: iced::Point::new(450., 400.),
bend: None,
route_segment: None,
},
);
mounted.settle(&mut app).await;
let token = app.model_menu.as_ref().unwrap().token;
send(
&mut app,
Message::Menu(crate::model_menu::Event::Invoke(
token,
Box::new(Message::StateDiagram(Event::Choose(
Tool::AdjustAttachment {
relation,
end: EdgeEnd::Source,
},
))),
)),
);
mounted.settle(&mut app).await;
assert!(app.model_menu.is_none());
assert_eq!(
app.behavior.diagram.tool,
Tool::AdjustAttachment {
relation,
end: EdgeEnd::Source
}
);
(app, mounted, relation)
}
fn canvas_focus(mounted: &mut Mounted, app: &App) -> (iced::Rectangle, bool) {
let id = iced::advanced::widget::Id::from("diagram-keyboard-owner");
let (_, bounds, focused) = mounted
.geometry(app)
.focused
.into_iter()
.find(|(candidate, _, _)| candidate.as_ref() == Some(&id))
.unwrap();
(bounds, focused)
}
#[tokio::test]
async fn mounted_menu_armed_attachment_escape_without_canvas_click_preserves_document_and_focus() {
let (mut app, mut mounted, _) = armed().await;
let before = app.document.snapshot();
let history = (app.document.can_undo(), app.document.can_redo());
let camera = (
app.behavior.diagram.zoom,
app.behavior.diagram.pan,
app.behavior.diagram.fit,
);
assert!(!canvas_focus(&mut mounted, &app).1);
let messages = press(&mut mounted, &mut app, Named::Escape).await;
assert!(messages.iter().any(|m| matches!(m, Message::Escape)));
assert_eq!(app.behavior.diagram.tool, Tool::Select);
assert!(!canvas_focus(&mut mounted, &app).1);
press(&mut mounted, &mut app, Named::Delete).await;
assert!(
!app.delete_pending,
"Escape must not grant canvas Delete ownership"
);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &before.bundle);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!((app.document.can_undo(), app.document.can_redo()), history);
assert_eq!(
(
app.behavior.diagram.zoom,
app.behavior.diagram.pan,
app.behavior.diagram.fit
),
camera
);
}
#[tokio::test]
async fn mounted_attachment_escape_preserves_modal_priority_and_focused_canvas_control() {
let (mut app, mut mounted, _) = armed().await;
let before = app.document.snapshot();
// Existing modal Escape consumes the first key without canceling the tool underneath.
send(&mut app, Message::Delete);
assert!(app.delete_pending && app.delete_target.is_some());
mounted.settle(&mut app).await;
press(&mut mounted, &mut app, Named::Escape).await;
assert!(!app.delete_pending);
assert!(matches!(
app.behavior.diagram.tool,
Tool::AdjustAttachment { .. }
));
// A real canvas click grants ownership; inner-canvas Escape remains live.
let bounds = canvas_focus(&mut mounted, &app).0;
mounted
.click(&mut app, iced::Point::new(bounds.x + 10., bounds.y + 10.))
.await;
assert!(canvas_focus(&mut mounted, &app).1);
let messages = press(&mut mounted, &mut app, Named::Escape).await;
assert!(
messages
.iter()
.any(|m| matches!(m, Message::StateDiagram(Event::Choose(Tool::Select))))
);
assert_eq!(app.behavior.diagram.tool, Tool::Select);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
}