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.
162 lines
5.6 KiB
Rust
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()));
|
|
}
|