Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/topology/layout_tests.rs
T

888 lines
36 KiB
Rust

use super::*;
#[tokio::test]
async fn production_process_canvas_scaled_path_creation_preserves_source_and_editor_bounds() {
use crate::behavior::canvas_workspace::{self, tests::Mounted};
use iced::Vector;
use iced::advanced::widget;
let initial = super::super::direct_tests::fixture(Layer::Oa);
let mut app = App::with_project(initial.p);
let _ = app.update(Message::Select(initial.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
let mut mounted = Mounted::new(&mut app).await;
for layer in [Layer::Oa, Layer::Sa] {
for size in [Size::new(1100., 700.), Size::new(1440., 900.)] {
let f = super::super::direct_tests::fixture(layer);
app = App::with_project(f.p);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = size;
mounted.settle(&mut app).await;
for scale in [0.5, 1., 2.] {
let context = app.topology_context().unwrap();
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Zoom(
context,
Some(scale),
)));
let geometry = mounted.geometry(&app);
let bounds = geometry.bounds("process-viewport");
assert!(bounds.height > 350.);
assert!(
!geometry
.scrolls
.contains_key(&widget::Id::new("process-canvas"))
);
let graph = Graph::new(app.project(), f.owner, f.owner);
let edge = graph.edges.iter().find(|edge| edge.id == f.link).unwrap();
let segment = edge
.points
.windows(2)
.max_by(|a, b| a[0].distance(a[1]).total_cmp(&b[0].distance(b[1])))
.unwrap();
let world = Point::new(
(segment[0].x + segment[1].x) / 2.,
(segment[0].y + segment[1].y) / 2.,
);
let desired = Vector::new(85., 130.);
let next_pan = desired - Vector::new(world.x, world.y) * scale;
let delta = next_pan - app.behavior.topology.pan;
let start = bounds.position() + Vector::new(130., 70.);
let snapshot = app.document.snapshot();
let source = app.document.bundle().clone();
mounted.pan(&mut app, start, start + delta).await;
let diff = app.behavior.topology.pan - next_pan;
assert!(diff.x.hypot(diff.y) < 0.01);
let tool = mounted.geometry(&app).bounds("behavior-tool:OR").center();
mounted.click(&mut app, tool).await;
let messages = mounted.click(&mut app, bounds.position() + desired).await;
assert!(
messages.iter().any(
|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Begin {
tool: Tool::Control, control: ControlKind::Choice, insertion, ..
}) if insertion.link == Some(f.link) && insertion.before.is_none())
),
"{layer:?} {scale}: {messages:?}"
);
let invocation = app.behavior.topology.draft.as_ref().unwrap().invocation;
let _ = app.update(event(Event::Name(" raw choice 雨 ".into())));
let raw = app.behavior.recovery_snapshot().unwrap();
mounted.settle(&mut app).await;
let shown = mounted.geometry(&app);
assert_eq!(shown.bounds("process-viewport"), bounds);
let actions = shown.bounds("process-actions");
let form = shown.scrolls[&widget::Id::new("process-form")].0;
assert!(actions.y >= bounds.y && actions.y + actions.height <= form.y);
assert!(form.y + form.height <= bounds.y + bounds.height && form.height > 250.);
let pan = app.behavior.topology.pan;
mounted
.batch(
&mut app,
&[canvas_workspace::tests::key(
iced::keyboard::key::Named::ArrowRight,
)],
mouse::Cursor::Available(bounds.position() + desired),
)
.await;
assert_eq!(app.behavior.topology.pan, pan);
mounted.scroll(&app, "process-form", 500.);
assert_eq!(mounted.geometry(&app).bounds("process-actions"), actions);
for shelf in [Shelf::Tools, Shelf::Selection, Shelf::Selection] {
let _ = app.update(event(Event::Shelf(shelf)));
assert_eq!(mounted.geometry(&app).bounds("process-viewport"), bounds);
assert_eq!(
app.behavior.topology.draft.as_ref().unwrap().invocation,
invocation
);
assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw);
}
let _ = app.update(event(Event::Cancel));
assert_eq!(mounted.geometry(&app).bounds("process-viewport"), bounds);
assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo() && !app.document.can_redo());
app.behavior.topology.shelf = None;
}
}
}
}
#[tokio::test]
async fn production_process_canvas_focus_wheel_fit_and_stale_camera_are_presentation_only() {
use crate::behavior::canvas_workspace::{
Event as Camera,
tests::{Mounted, key},
};
use iced::Vector;
let f = super::super::direct_tests::fixture(Layer::Sa);
let mut app = App::with_project(f.p);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = Size::new(1100., 700.);
let mut mounted = Mounted::new(&mut app).await;
let bounds = mounted.geometry(&app).bounds("process-viewport");
let point = bounds.position() + Vector::new(90., 120.);
let accepted = app.document.snapshot();
let bundle = app.document.bundle().clone();
mounted
.pan(&mut app, point, point + Vector::new(-20., 25.))
.await;
let pan = app.behavior.topology.pan;
mounted
.batch(
&mut app,
&[key(iced::keyboard::key::Named::ArrowRight)],
mouse::Cursor::Available(point),
)
.await;
assert_eq!(app.behavior.topology.pan, pan + Vector::new(-60., 0.));
let local = point - bounds.position();
let pan = app.behavior.topology.pan;
let old_scale = app.behavior.topology.zoom.unwrap_or(1.);
let before_world = (local - pan) / old_scale;
mounted
.batch(
&mut app,
&[
iced::Event::Mouse(mouse::Event::CursorMoved { position: point }),
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
}),
],
mouse::Cursor::Available(point),
)
.await;
let after_world = (local - app.behavior.topology.pan) / app.behavior.topology.zoom.unwrap();
let diff = after_world - before_world;
assert!(diff.x.hypot(diff.y) < 0.001);
let context = app.topology_context().unwrap();
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Zoom(context, None)));
let fitted = (app.behavior.topology.pan, app.behavior.topology.zoom);
let _ = app.update(event(Event::Shelf(Shelf::Selection)));
mounted.settle(&mut app).await;
app.window_size = Size::new(1440., 900.);
mounted.settle(&mut app).await;
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
fitted,
"Fit is a one-time camera action"
);
for mismatch in 0..3 {
let mut stale = app.topology_context().unwrap();
match mismatch {
0 => stale.document = Id::new_v4(),
1 => stale.owner = Id::new_v4(),
_ => stale.revision += 1,
}
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Camera(
stale,
app.camera_epoch,
Camera::Pan(Vector::new(400., 400.)),
)));
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
fitted
);
}
app.hosted.visible = true;
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Camera(
app.topology_context().unwrap(),
app.camera_epoch,
Camera::Pan(Vector::new(30., 40.)),
)));
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
fitted
);
assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &bundle);
assert!(!app.document.can_undo() && !app.document.can_redo());
}
#[tokio::test]
async fn production_process_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags() {
use crate::behavior::canvas_workspace::tests::*;
let f = super::super::direct_tests::fixture(Layer::Oa);
let mut app = App::with_project(f.p);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = Size::new(1100., 700.);
let mut mounted = Mounted::new(&mut app).await;
let snapshot = app.document.snapshot();
let source = app.document.bundle().clone();
for boundary in ["Fit", "OR", "Details", "Panels", "Bounds"] {
let toolbar = capture_toolbar(&mut mounted, &app, "Fit");
let bounds = mounted.geometry(&app).bounds("process-viewport");
let point = bounds.position() + iced::Vector::new(95., 90.);
let lifetime = app.camera_epoch;
let pan = app.behavior.topology.pan;
let first = point + iced::Vector::new(13., 9.);
let last = first + iced::Vector::new(17., -3.);
let emitted = mounted
.batch(
&mut app,
&[
iced::Event::Mouse(mouse::Event::CursorMoved { position: point }),
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)),
iced::Event::Mouse(mouse::Event::CursorMoved { position: first }),
iced::Event::Mouse(mouse::Event::CursorMoved { position: last }),
],
mouse::Cursor::Available(last),
)
.await;
assert_eq!(app.behavior.topology.pan, pan + (last - point));
assert_eq!(
app.camera_epoch, lifetime,
"own ordered deltas retain their lifetime"
);
let retained = camera_delta(&emitted);
if boundary == "Bounds" {
let size = Size::new(app.window_size.width + 90., app.window_size.height + 35.);
let task = app.update(Message::WindowResized(size));
mounted.perform(&mut app, task).await;
} else {
let id = scope(&app).control_id(boundary);
let at = mounted.geometry(&app).containers[&id].center();
let clicked = mounted.click(&mut app, at).await;
assert!(
focused(&mut mounted, &app, boundary),
"same-batch pointer activation focuses the resulting live control without F6"
);
if boundary == "Fit" {
mounted
.batch(
&mut app,
&[key(iced::keyboard::key::Named::Tab)],
mouse::Cursor::Unavailable,
)
.await;
assert!(
focused(&mut mounted, &app, "ZoomOut"),
"Tab after the real press/release batch continues from Fit"
);
assert!(clicked.iter().any(|m| matches!(
intent_action(m),
Message::TopologyCanvas(CanvasEvent::Zoom(_, None))
)));
assert_eq!(
app.behavior.topology.pan,
iced::Vector::ZERO,
"Fit emitted after this same batch's old pan must still execute"
);
}
}
mounted.settle(&mut app).await;
assert_ne!(
app.camera_epoch, lifetime,
"{boundary} ends the old input lifetime"
);
let camera = (app.behavior.topology.pan, app.behavior.topology.zoom);
let bounds = mounted.geometry(&app).bounds("process-viewport");
let released = release_middle(
&mut mounted,
&mut app,
bounds.position() + iced::Vector::new(130., 145.),
)
.await;
assert!(!released.iter().any(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(_, _, crate::behavior::canvas_workspace::Event::Pan(delta))) if *delta != iced::Vector::ZERO)),
"{boundary} must cancel the retained widget drag");
let task = app.update(retained);
mounted.perform(&mut app, task).await;
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
camera,
"{boundary} must reject its pre-boundary App delta"
);
let current_lifetime = app.camera_epoch;
let task = app.update(toolbar);
mounted.perform(&mut app, task).await;
assert_eq!(
app.camera_epoch, current_lifetime,
"{boundary} rejects the retained toolbar lifetime"
);
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
camera,
"{boundary} retained Fit cannot alter the camera"
);
}
assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo() && !app.document.can_redo());
}
#[tokio::test]
async fn production_process_keyboard_enters_activates_draws_focus_and_returns_to_inspector() {
use crate::behavior::canvas_workspace::tests::*;
use iced::keyboard::key::Named;
let f = super::super::direct_tests::fixture(Layer::Oa);
let mut app = App::with_project(f.p);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = Size::new(1100., 700.);
let mut mounted = Mounted::new(&mut app).await;
let snapshot = app.document.snapshot();
let source = app.document.bundle().clone();
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
assert!(!focused(&mut mounted, &app, "viewport"));
let operations = mounted.operations;
mounted
.batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable)
.await;
assert!(
mounted.operations > operations,
"entry executes the returned App widget Task"
);
assert!(focused(&mut mounted, &app, "viewport"));
let mut backwards = key(Named::Tab);
if let iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { modifiers, .. }) =
&mut backwards
{
*modifiers = iced::keyboard::Modifiers::SHIFT;
}
mounted
.batch(&mut app, &[backwards], mouse::Cursor::Unavailable)
.await;
assert!(focused(&mut mounted, &app, "ActualSize"));
mounted
.batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable)
.await;
assert!(focused(&mut mounted, &app, "viewport"));
assert_eq!(
mounted
.geometry(&app)
.focused
.iter()
.filter(|(_, _, focused)| *focused)
.count(),
1
);
tab_to(&mut mounted, &mut app, "OR").await;
let chosen = mounted
.batch(&mut app, &[key(Named::Enter)], mouse::Cursor::Unavailable)
.await;
assert!(chosen.iter().any(|m| matches!(
intent_action(m),
Message::TopologyCanvas(CanvasEvent::Choose(..))
)));
assert!(
focused(&mut mounted, &app, "OR"),
"tool activation retains its keyboard target"
);
// The selected palette tool must still have a visibly distinct focus ring.
let tool_on = mounted.pixels(&app);
let geometry = mounted.geometry(&app);
let mut tool_bounds = geometry.containers[&scope(&app).control_id("OR")];
let offset = geometry.scrolls[&scope(&app).control_id("palette-scroll")].1;
tool_bounds.x -= offset.x;
tool_bounds.y -= offset.y;
mounted
.batch(&mut app, &[key(Named::Escape)], mouse::Cursor::Unavailable)
.await;
let tool_off = mounted.pixels(&app);
assert_drawn_focus(&tool_off, &tool_on, app.window_size, tool_bounds);
mounted
.batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable)
.await;
// Focus alone changes the visible border of the real production Fit control.
let before = mounted.pixels(&app);
tab_to(&mut mounted, &mut app, "Fit").await;
let id = scope(&app).control_id("Fit");
let bounds = mounted.geometry(&app).containers[&id];
let after = mounted.pixels(&app);
assert_drawn_focus(&before, &after, app.window_size, bounds);
let fitted = mounted
.batch(&mut app, &[key(Named::Space)], mouse::Cursor::Unavailable)
.await;
assert!(fitted.iter().any(|m| matches!(
intent_action(m),
Message::TopologyCanvas(CanvasEvent::Zoom(_, None))
)));
assert_eq!(app.behavior.topology.pan, iced::Vector::ZERO);
assert!(focused(&mut mounted, &app, "Fit"));
tab_to(&mut mounted, &mut app, "Canvas").await;
mounted
.batch(&mut app, &[key(Named::Enter)], mouse::Cursor::Unavailable)
.await;
assert!(focused(&mut mounted, &app, "viewport"));
mounted
.batch(&mut app, &[key(Named::Escape)], mouse::Cursor::Unavailable)
.await;
assert!(!focused(&mut mounted, &app, "viewport"));
mounted
.batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable)
.await;
assert!(
mounted
.geometry(&app)
.focused
.iter()
.any(|(_, _, focused)| *focused),
"ordinary Tab resumes the production Inspector's own entry policy"
);
assert!(!focused(&mut mounted, &app, "viewport"));
mounted
.batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable)
.await;
assert!(focused(&mut mounted, &app, "viewport"));
assert_eq!(
mounted
.geometry(&app)
.focused
.iter()
.filter(|(_, _, focused)| *focused)
.count(),
1,
"explicit canvas entry releases Inspector keyboard ownership"
);
mounted
.batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable)
.await;
assert!(!focused(&mut mounted, &app, "viewport"));
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
// Typing after exit belongs to Inspector. Replaying the prior clean tool
// action must retain those raw fields, never implicitly Apply them.
mounted
.batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable)
.await;
mounted
.batch(&mut app, &[character("x")], mouse::Cursor::Unavailable)
.await;
assert!(app.draft_dirty());
let dirty_raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
assert_ne!(dirty_raw, raw);
let camera = (app.behavior.topology.pan, app.behavior.topology.zoom);
for message in chosen {
let task = app.update(message);
mounted.perform(&mut app, task).await;
}
let emitted = mounted
.batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable)
.await;
assert!(
!emitted
.iter()
.any(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(..))))
);
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
camera
);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
dirty_raw
);
assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo() && !app.document.can_redo());
}
#[tokio::test]
async fn production_process_caller_return_rejects_its_previous_camera_lifetime() {
use crate::behavior::canvas_workspace::tests::*;
let f = super::super::direct_tests::fixture(Layer::Oa);
let mut project = f.p;
let target = project
.add_element(Kind::Process, Layer::Oa, "Reusable check", None)
.unwrap();
let reference = occurrence(
&project,
f.owner,
"Local use",
OccurrenceBody::SequenceReference {
sequence: Some(target),
},
);
let use_id = reference.id;
project = execute_batch(&project, &[Command::CreateElement(reference)]).unwrap();
let mut app = App::with_project(project);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = Size::new(1100., 700.);
let mut mounted = Mounted::new(&mut app).await;
let source = app.document.bundle().clone();
let snapshot = app.document.snapshot();
let bounds = mounted.geometry(&app).bounds("process-viewport");
let point = bounds.position() + iced::Vector::new(80., 100.);
let emitted = mounted
.pan(&mut app, point, point + iced::Vector::new(35., -25.))
.await;
let retained = camera_delta(&emitted);
let camera = (app.behavior.topology.pan, app.behavior.topology.zoom);
let toolbar = capture_toolbar(&mut mounted, &app, "Fit");
let task = app.update(Message::TopologyCanvas(CanvasEvent::Open(
app.topology_context().unwrap(),
use_id,
)));
mounted.perform(&mut app, task).await;
mounted.settle(&mut app).await;
assert_eq!(app.topology_context().unwrap().owner, target);
let target_bounds = mounted.geometry(&app).bounds("process-viewport");
let point = target_bounds.position() + iced::Vector::new(70., 90.);
mounted
.pan(&mut app, point, point + iced::Vector::new(-50., 20.))
.await;
let task = app.update(Message::TopologyCanvas(CanvasEvent::Back(
app.topology_context().unwrap(),
)));
mounted.perform(&mut app, task).await;
mounted.settle(&mut app).await;
assert_eq!(app.topology_context().unwrap().owner, f.owner);
assert_eq!(app.selected, use_id);
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
camera
);
let task = app.update(retained);
mounted.perform(&mut app, task).await;
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
camera
);
let lifetime = app.camera_epoch;
let task = app.update(toolbar);
mounted.perform(&mut app, task).await;
assert_eq!(
app.camera_epoch, lifetime,
"A to B and Back cannot revive toolbar intent"
);
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
camera
);
mounted
.pan(&mut app, point, point + iced::Vector::new(12., -8.))
.await;
assert_eq!(
app.behavior.topology.pan,
camera.0 + iced::Vector::new(12., -8.)
);
assert_eq!(app.document.bundle(), &source);
assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert!(!app.document.can_undo() && !app.document.can_redo());
}
#[tokio::test]
async fn production_canvas_keyboard_and_delayed_tasks_respect_real_chooser_and_review_owners() {
use crate::behavior::canvas_workspace::tests::*;
use iced::keyboard::key::Named;
let f = super::super::direct_tests::fixture(Layer::Oa);
let mut project = f.p;
let target = project
.add_element(Kind::FunctionalChain, Layer::Sa, "Counterpart", None)
.unwrap();
let trace = Id::new_v4();
project.traces.insert(
trace,
Trace {
id: trace,
alias: "canvas_focus_trace".into(),
name: "Review camera ownership".into(),
description: String::new(),
oa: f.owner,
sa: target,
kind: TraceKind::Manual,
baseline: None,
},
);
let mut app = App::with_project(project);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = Size::new(1440., 900.);
let mut mounted = Mounted::new(&mut app).await;
let source = app.document.bundle().clone();
let snapshot = app.document.snapshot();
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
let mut retained_apply = retained_focus_apply(&mut mounted, &mut app).await;
let entry = mounted.input(&app, &[key(Named::F6)], mouse::Cursor::Unavailable);
assert_eq!(entry.len(), 1);
let delayed = app.update(entry[0].clone());
let draft = app.draft.as_ref().unwrap();
let parent_id =
crate::forms::reference_chooser::trigger::field_id(draft.reference_generation(), "/parent");
let parent_bounds = mounted
.geometry(&app)
.focused
.iter()
.find(|(id, _, _)| id.as_ref() == Some(&parent_id))
.expect("actual Inspector reference trigger is mounted")
.1;
let request = draft.reference_open(app.project(), "/parent", parent_bounds);
let point =
mounted.geometry(&app).bounds("process-viewport").position() + iced::Vector::new(80., 100.);
press_middle(&mut mounted, &mut app, point).await;
let moved = point + iced::Vector::new(12., 8.);
let emitted = mounted
.batch(
&mut app,
&[iced::Event::Mouse(mouse::Event::CursorMoved {
position: moved,
})],
mouse::Cursor::Available(moved),
)
.await;
let retained = camera_delta(&emitted);
let root_inputs = mounted.geometry(&app).text_inputs;
let task = app.update(Message::FieldReferenceOpen(request));
mounted.perform(&mut app, task).await;
assert!(
app.reference_input_active(),
"the real App chooser must open"
);
let popup_focus = mounted
.geometry(&app)
.focused
.into_iter()
.filter(|(_, _, value)| *value)
.collect::<Vec<_>>();
assert!(
popup_focus
.iter()
.any(|(id, bounds, focused)| id.is_none() && *focused && !root_inputs.contains(bounds)),
"the newly mounted nested chooser search, absent from root inputs, must be focused"
);
mounted.perform(&mut app, delayed).await;
mounted.operate(&app, &mut *retained_apply);
assert_eq!(
mounted
.geometry(&app)
.focused
.into_iter()
.filter(|(_, _, value)| *value)
.collect::<Vec<_>>(),
popup_focus,
"both delayed Collect and deliberately retained Apply respect the actual nested overlay owner"
);
assert!(!focused(&mut mounted, &app, "viewport"));
let camera = (app.behavior.topology.pan, app.behavior.topology.zoom);
for key_name in [Named::F6, Named::Tab, Named::ArrowRight] {
let emitted = mounted
.batch(&mut app, &[key(key_name)], mouse::Cursor::Unavailable)
.await;
assert!(
!emitted
.iter()
.any(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(..))))
);
}
// Tab may dismiss the chooser to its owning field; Escape closes it if still open.
if app.reference_input_active() {
mounted
.batch(&mut app, &[key(Named::Escape)], mouse::Cursor::Unavailable)
.await;
}
assert!(!app.reference_input_active());
let returned_focus = mounted
.geometry(&app)
.focused
.into_iter()
.filter(|(_, _, value)| *value)
.collect::<Vec<_>>();
mounted.operate(&app, &mut *retained_apply);
assert_eq!(
mounted
.geometry(&app)
.focused
.into_iter()
.filter(|(_, _, value)| *value)
.collect::<Vec<_>>(),
returned_focus,
"the same owner after chooser Back has a different lifetime; retained Apply must stay inert"
);
release_middle(&mut mounted, &mut app, point + iced::Vector::new(50., 35.)).await;
let task = app.update(retained);
mounted.perform(&mut app, task).await;
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
camera,
"a disabled-and-reenabled canvas cannot resume its pre-chooser pan"
);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
let mut retained_apply = retained_focus_apply(&mut mounted, &mut app).await;
let entry = mounted.input(&app, &[key(Named::F6)], mouse::Cursor::Unavailable);
let delayed = app.update(
entry
.into_iter()
.find(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(..))))
.unwrap(),
);
let task = app.update(Message::StartSemanticReview(trace));
mounted.perform(&mut app, task).await;
let facet = FacetSelector::Header(HeaderFacet::Parent);
for row in [
crate::semantic_review::RowEvent::Select(facet.clone()),
crate::semantic_review::RowEvent::Decide(facet, Some(SemanticDecision::Candidate)),
] {
let Some(crate::workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else {
panic!("actual Review dialog opens");
};
let message = state.address_event(row);
let task = app.update(Message::Workflow(crate::workflow::Event::semantic(message)));
mounted.perform(&mut app, task).await;
}
let Some(crate::workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else {
panic!("Review remains open");
};
let review = state.recovery_snapshot().unwrap();
let review_id = crate::forms::reference_chooser::trigger::field_id(
state
.reference_form(app.project())
.unwrap()
.reference_generation(),
"/parent",
);
mounted
.batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable)
.await;
assert!(
mounted
.geometry(&app)
.focused
.iter()
.any(|(id, _, focused)| id.as_ref() == Some(&review_id) && *focused)
);
let scrolls = mounted.geometry(&app).scrolls;
mounted.perform(&mut app, delayed).await;
mounted.operate(&app, &mut *retained_apply);
assert_eq!(
mounted.geometry(&app).scrolls,
scrolls,
"a retained Apply must not scroll the new owner"
);
for key_name in [Named::F6, Named::ArrowRight] {
let emitted = mounted
.batch(&mut app, &[key(key_name)], mouse::Cursor::Unavailable)
.await;
assert!(
!emitted
.iter()
.any(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(..))))
);
}
assert!(
mounted
.geometry(&app)
.focused
.iter()
.any(|(id, _, focused)| id.as_ref() == Some(&review_id) && *focused)
);
let Some(crate::workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else {
panic!("Review retains ownership");
};
assert_eq!(state.recovery_snapshot().unwrap(), review);
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
camera
);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert_eq!(app.document.bundle(), &source);
assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert!(!app.document.can_undo() && !app.document.can_redo());
}
#[tokio::test]
async fn production_process_form_replacement_resets_scroll_without_a_closed_render() {
use crate::behavior::canvas_workspace::tests::Mounted;
use iced::advanced::widget;
let f = super::super::direct_tests::fixture(Layer::Oa);
let mut app = App::with_project(f.p);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = Size::new(1100., 520.);
let mut mounted = Mounted::new(&mut app).await;
let source = app.document.bundle().clone();
let snapshot = app.document.snapshot();
let bounds = mounted.geometry(&app).bounds("process-viewport");
let point = bounds.position() + iced::Vector::new(65., 80.);
mounted
.pan(&mut app, point, point + iced::Vector::new(-15., 24.))
.await;
let camera = (app.behavior.topology.pan, app.behavior.topology.zoom);
let task = app.update(event(Event::Start(f.owner, Tool::Branch, None)));
mounted.perform(&mut app, task).await;
mounted.settle(&mut app).await;
let session = app.behavior.topology.draft.as_ref().unwrap().invocation;
mounted.scroll(&app, "process-form", 700.);
let offset = mounted.geometry(&app).scrolls[&widget::Id::new("process-form")].1;
assert!(
offset.y > 0.,
"real production Branch fields must overflow this viewport"
);
let task = app.update(event(Event::Name("Preserve this same session".into())));
mounted.perform(&mut app, task).await;
assert_eq!(
mounted.geometry(&app).scrolls[&widget::Id::new("process-form")].1,
offset
);
assert_eq!(
app.behavior.topology.draft.as_ref().unwrap().invocation,
session
);
let task = app.update(event(Event::Cancel));
mounted.perform(&mut app, task).await;
let task = app.update(event(Event::Start(f.owner, Tool::Branch, None)));
mounted.perform(&mut app, task).await;
assert_ne!(
app.behavior.topology.draft.as_ref().unwrap().invocation,
session
);
// No closed-state App::view was rendered between Cancel and Start.
let geometry = mounted.geometry(&app);
assert_eq!(
geometry.scrolls[&widget::Id::new("process-form")].1,
iced::Vector::ZERO
);
assert_eq!(geometry.bounds("process-viewport"), bounds);
assert_eq!(
(app.behavior.topology.pan, app.behavior.topology.zoom),
camera
);
assert_eq!(app.document.bundle(), &source);
assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert!(!app.document.can_undo() && !app.document.can_redo());
}
#[tokio::test]
async fn production_process_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes() {
use crate::behavior::canvas_workspace::tests::*;
let f = super::super::direct_tests::fixture(Layer::Oa);
let mut app = App::with_project(f.p);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = Size::new(1440., 900.);
let mut mounted = Mounted::new(&mut app).await;
let snapshot = app.document.snapshot();
let source = app.document.bundle().clone();
retained_toolbar_boundaries(&mut mounted, &mut app, "OR").await;
assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo() && !app.document.can_redo());
}
#[tokio::test]
async fn production_process_keyboard_reveals_and_activates_initially_offscreen_last_tool() {
use crate::behavior::canvas_workspace::tests::*;
let f = super::super::direct_tests::fixture(Layer::Oa);
let mut app = App::with_project(f.p);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = Size::new(1100., 420.);
let mut mounted = Mounted::new(&mut app).await;
let snapshot = app.document.snapshot();
let source = app.document.bundle().clone();
overflow_last_tool(&mut mounted, &mut app, "->").await;
assert_eq!(app.behavior.topology.tool, CanvasTool::Connect);
assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo() && !app.document.can_redo());
}