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::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::>(); 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::>(), 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::>(); mounted.operate(&app, &mut *retained_apply); assert_eq!( mounted .geometry(&app) .focused .into_iter() .filter(|(_, _, value)| *value) .collect::>(), 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()); }