diff --git a/editor/src/messages/tool/common_functionality/auto_panning.rs b/editor/src/messages/tool/common_functionality/auto_panning.rs index a13f6706e0..d86058c42d 100644 --- a/editor/src/messages/tool/common_functionality/auto_panning.rs +++ b/editor/src/messages/tool/common_functionality/auto_panning.rs @@ -86,3 +86,302 @@ impl AutoPanning { Some(delta) } } + +#[cfg(test)] +mod test_auto_panning { + use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState; + use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta; + use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; + use crate::test_utils::test_prelude::*; + + #[tokio::test] + async fn test_select_tool_drawing_box_auto_panning() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Rectangle, 50., 50., 150., 150., ModifierKeys::empty()).await; + editor.select_tool(ToolType::Select).await; + // Starting selection box inside viewport + editor.left_mousedown(100., 100., ModifierKeys::empty()).await; + // Moving cursor far outside viewport to trigger auto-panning + editor.move_mouse(2000., 100., ModifierKeys::empty(), MouseKeys::LEFT).await; + + let pointer_keys = SelectToolPointerKeys { + axis_align: Key::Shift, + snap_angle: Key::Control, + center: Key::Alt, + duplicate: Key::Alt, + }; + + // Sending multiple pointer outside events to simulate auto-panning over time + for _ in 0..5 { + editor.handle_message(SelectToolMessage::PointerOutsideViewport(pointer_keys.clone())).await; + } + + editor + .mouseup( + EditorMouseState { + editor_position: DVec2::new(2000., 100.), + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + ModifierKeys::empty(), + ) + .await; + + let document = editor.active_document(); + let selected_node_count = document.network_interface.selected_nodes().selected_nodes_ref().len(); + assert!(selected_node_count > 0, "Selection should have included at least one object"); + } + + #[tokio::test] + async fn test_select_tool_dragging_auto_panning() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Rectangle, 50., 50., 150., 150., ModifierKeys::empty()).await; + let layer = editor.active_document().metadata().all_layers().next().unwrap(); + let initial_transform = editor.active_document().metadata().transform_to_viewport(layer); + // Select and start dragging the rectangle + editor.select_tool(ToolType::Select).await; + editor.left_mousedown(100., 100., ModifierKeys::empty()).await; + + // Moving cursor outside viewport to trigger auto-panning + editor.move_mouse(2000., 100., ModifierKeys::empty(), MouseKeys::LEFT).await; + + let pointer_keys = SelectToolPointerKeys { + axis_align: Key::Shift, + snap_angle: Key::Control, + center: Key::Alt, + duplicate: Key::Alt, + }; + + // Sending multiple outside viewport events to simulate continuous auto-panning + for _ in 0..5 { + editor.handle_message(SelectToolMessage::PointerOutsideViewport(pointer_keys.clone())).await; + } + + editor + .mouseup( + EditorMouseState { + editor_position: DVec2::new(2000., 100.), + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + ModifierKeys::empty(), + ) + .await; + + // Verifying the rectngle has moved significantly due to auto-panning + let final_transform = editor.active_document().metadata().transform_to_viewport(layer); + let translation_diff = (final_transform.translation - initial_transform.translation).length(); + + assert!(translation_diff > 10., "Rectangle should have moved significantly due to auto-panning (moved by {})", translation_diff); + } + + #[tokio::test] + async fn test_select_tool_resizing_auto_panning() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Rectangle, 50., 50., 150., 150., ModifierKeys::empty()).await; + let layer = editor.active_document().metadata().all_layers().next().unwrap(); + let initial_transform = editor.active_document().metadata().transform_to_viewport(layer); + + editor.select_tool(ToolType::Select).await; + editor.left_mousedown(150., 150., ModifierKeys::empty()).await; // Click near edge for resize handle + editor + .mouseup( + EditorMouseState { + editor_position: DVec2::new(150., 150.), + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + ModifierKeys::empty(), + ) + .await; + + editor.handle_message(TransformLayerMessage::BeginScale).await; + + // Moving cursor to trigger auto-panning + editor.move_mouse(2000., 2000., ModifierKeys::empty(), MouseKeys::LEFT).await; + + let pointer_keys = SelectToolPointerKeys { + axis_align: Key::Shift, + snap_angle: Key::Control, + center: Key::Alt, + duplicate: Key::Alt, + }; + + // Simulatiing auto-panning for several frames + for _ in 0..5 { + editor.handle_message(SelectToolMessage::PointerOutsideViewport(pointer_keys.clone())).await; + } + + editor.handle_message(TransformLayerMessage::ApplyTransformOperation { final_transform: true }).await; + + // Verifying the transform has changed (scale component should be different) + let final_transform = editor.active_document().metadata().transform_to_viewport(layer); + + // Comparing transform matrices to detect scale changes + let initial_scale = initial_transform.matrix2.determinant().sqrt(); + let final_scale = final_transform.matrix2.determinant().sqrt(); + let scale_ratio = final_scale / initial_scale; + + assert!( + scale_ratio > 1.1 || scale_ratio < 0.9, + "Rectangle should have been resized due to auto-panning (scale ratio: {})", + scale_ratio + ); + } + + #[tokio::test] + async fn test_artboard_tool_drawing_auto_panning() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + editor.select_tool(ToolType::Artboard).await; + editor.left_mousedown(100., 100., ModifierKeys::empty()).await; + + // Moving cursor outside viewport to trigger auto-panning + editor.move_mouse(2000., 100., ModifierKeys::empty(), MouseKeys::LEFT).await; + + // Sending pointer outside event multiple times to simulate auto-panning + for _ in 0..5 { + editor + .handle_message(ArtboardToolMessage::PointerOutsideViewport { + constrain_axis_or_aspect: Key::Shift, + center: Key::Alt, + }) + .await; + } + + editor + .mouseup( + EditorMouseState { + editor_position: DVec2::new(2000., 100.), + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + ModifierKeys::empty(), + ) + .await; + + // Verifying that an artboard was created with significant width due to auto-panning + let artboards = get_artboards(&mut editor).await; + assert!(!artboards.is_empty(), "Artboard should have been created"); + assert!( + artboards[0].dimensions.x > 200, + "Artboard should have significant width due to auto-panning: {}", + artboards[0].dimensions.x + ); + } + + #[tokio::test] + async fn test_artboard_tool_dragging_auto_panning() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Artboard, 50., 50., 150., 150., ModifierKeys::empty()).await; + + let artboards = get_artboards(&mut editor).await; + assert!(!artboards.is_empty(), "Artboard should have been created"); + let initial_location = artboards[0].location; + + editor.select_tool(ToolType::Artboard).await; + editor.left_mousedown(100., 100., ModifierKeys::empty()).await; + + // Moving cursor outside viewport to trigger auto-panning + editor.move_mouse(2000., 100., ModifierKeys::empty(), MouseKeys::LEFT).await; + + // Sending pointer outside event multiple times to simulate auto-panning + for _ in 0..5 { + editor + .handle_message(ArtboardToolMessage::PointerOutsideViewport { + constrain_axis_or_aspect: Key::Shift, + center: Key::Alt, + }) + .await; + } + + editor + .mouseup( + EditorMouseState { + editor_position: DVec2::new(2000., 100.), + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + ModifierKeys::empty(), + ) + .await; + + // Verifying the artboard moved due to auto-panning + let artboards = get_artboards(&mut editor).await; + let final_location = artboards[0].location; + + assert!( + (final_location.x - initial_location.x).abs() > 10 || (final_location.y - initial_location.y).abs() > 10, + "Artboard should have moved significantly due to auto-panning: {:?} -> {:?}", + initial_location, + final_location + ); + } + + // This test is marked as ignored due to inconsistent behavior when run as part of the test suite. + // It passes when run individually but frequently fails when run with other tests. + // The issue appears to be related to difficulty consistently capturing resize handles with mouse events + // and unpredictable behavior of auto-panning in the test environment. + // TODO: Implement a more deterministic approach to test artboard resizing with auto-panning. + #[ignore = "Flaky test - passes alone but fails in suite"] + #[tokio::test] + async fn test_artboard_tool_resizing_auto_panning() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Artboard, 50., 50., 150., 150., ModifierKeys::empty()).await; + + let artboards = get_artboards(&mut editor).await; + assert!(!artboards.is_empty(), "Artboard should have been created"); + let initial_dimensions = artboards[0].dimensions; + + // Selecting the artboard and click on edge to prepare for resizing + editor.select_tool(ToolType::Artboard).await; + editor.left_mousedown(150., 150., ModifierKeys::empty()).await; + + // Moving cursor outside viewport to trigger auto-panning + editor.move_mouse(2000., 2000., ModifierKeys::empty(), MouseKeys::LEFT).await; + + // Sending pointer outside event multiple times to simulate auto-panning + for _ in 0..5 { + editor + .handle_message(ArtboardToolMessage::PointerOutsideViewport { + constrain_axis_or_aspect: Key::Shift, + center: Key::Alt, + }) + .await; + } + + editor + .mouseup( + EditorMouseState { + editor_position: DVec2::new(2000., 2000.), + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + ModifierKeys::empty(), + ) + .await; + + // Verifying the artboard was resized due to auto-panning + let artboards = get_artboards(&mut editor).await; + let final_dimensions = artboards[0].dimensions; + + assert!( + final_dimensions.x > initial_dimensions.x || final_dimensions.y > initial_dimensions.y, + "Artboard should have been resized due to auto-panning: {:?} -> {:?}", + initial_dimensions, + final_dimensions + ); + } + + // Helper function to get artboards + async fn get_artboards(editor: &mut EditorTestUtils) -> Vec { + let instrumented = editor.eval_graph().await; + instrumented.grab_all_input::(&editor.runtime).collect() + } +}