enso/integration-test/tests/graph_editor.rs
Paweł Grabarz fcaa7510c5
Do not immediately modify code when disconnecting the edge at target side. (#7041)
Fixes #6772

When detaching an existing edge by grabbing by a source port, the node's code is no longer immediately modified. It is only changed once the edge has been either connected or destroyed. When grabbing on the source side, the existing behavior is preserved. That way, we always have guaranteed place to keep the edge connected to.

https://github.com/enso-org/enso/assets/919491/49e560cb-0a29-4c6a-97ec-4370185b8c89

In general, the detached edges are now more stable, resilient to all kinds of expression modifications during the drag.

https://github.com/enso-org/enso/assets/919491/e62450ff-46b2-466f-ac33-f4f19e66ee1d


In case there is a situation where the currently dragged edge's port is destroyed (e.g. by Undo/Redo), instead of showing glitched port position it is simply dropped.

https://github.com/enso-org/enso/assets/919491/8fb089aa-a4a5-4a8c-92eb-23aeff9867b8

# Important Notes

The whole edge connection and view handling at the graph-editor view level has been completely rewritten. The edge endpoints are now identified using new `PortId` structure, that is not dependant on the span-tree. This prepares us for eventual removal of the span-tree in favour of manipulating AST directly. Right now those `PortId`s are still stored within the span-tree nodes, but it will be easy to eventually generate them on the fly from the AST itself. The widget tree has also already been switched to that representation where appropriate.

Additionally, I have started splitting the graph editor FRP network into smaller methods. Due to its absolutely enormous size and complexity of it, I haven't finished the split completely, and mostly edge-related part is refactored. I don't want to block this PR on this any longer though, as the merge conflicts are getting a bit unwieldy to deal with.
2023-06-20 21:27:39 +00:00

345 lines
14 KiB
Rust

// === Non-Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use enso_gui::integration_test::prelude::*;
use approx::assert_abs_diff_eq;
use enso_frp::future::FutureEvent;
use enso_frp::io::mouse::PrimaryButton;
use enso_frp::stream::ValueProvider;
use enso_gui::view::graph_editor::component::node as node_view;
use enso_gui::view::graph_editor::component::node::test_utils::NodeModelExt;
use enso_gui::view::graph_editor::GraphEditor;
use enso_gui::view::graph_editor::Node;
use enso_gui::view::graph_editor::NodeId;
use enso_gui::view::graph_editor::NodeSource;
use enso_web::sleep;
use ensogl::display::navigation::navigator::ZoomEvent;
use ensogl::display::scene::test_utils::MouseExt;
use ensogl::display::Scene;
use std::time::Duration;
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn create_new_project_and_add_nodes() {
let test = Fixture::setup_new_project().await;
let graph_editor = test.graph_editor();
assert_eq!(graph_editor.model.nodes.len(), 2);
let expect_node_added = graph_editor.node_added.next_event();
graph_editor.add_node();
let (added_node_id, source_node, _) = expect_node_added.expect();
assert_eq!(source_node, None);
assert_eq!(graph_editor.model.nodes.len(), 3);
let added_node =
graph_editor.model.nodes.get_cloned_ref(&added_node_id).expect("Added node is not added");
assert_eq!(added_node.view.set_expression.value().code, "");
}
#[wasm_bindgen_test]
async fn debug_mode() {
let test = Fixture::setup_new_project().await;
let project = test.project_view();
let graph_editor = test.graph_editor();
assert!(!graph_editor.debug_mode.value());
// Turning On
let expect_mode = project.debug_mode.next_event();
let expect_popup_message = project.debug_mode_popup().content_frp_node().next_event();
project.enable_debug_mode();
assert!(expect_mode.expect());
let message = expect_popup_message.expect();
assert!(
message.contains("Debug Mode enabled"),
"Message \"{message}\" does not mention enabling Debug mode"
);
assert!(
message.contains(enso_gui::view::debug_mode_popup::DEBUG_MODE_SHORTCUT),
"Message \"{message}\" does not inform about shortcut to turn mode off"
);
assert!(graph_editor.debug_mode.value());
// Turning Off
let expect_mode = project.debug_mode.next_event();
let expect_popup_message = project.debug_mode_popup().content_frp_node().next_event();
project.disable_debug_mode();
assert!(!expect_mode.expect());
let message = expect_popup_message.expect();
assert!(
message.contains("Debug Mode disabled"),
"Message \"{message}\" does not mention disabling of debug mode"
);
assert!(!graph_editor.debug_mode.value());
}
#[wasm_bindgen_test]
async fn zooming() {
let test = Fixture::setup_new_project().await;
let project = test.project_view();
let graph_editor = test.graph_editor();
let camera = test.ide.ensogl_app.display.default_scene.layers.main.camera();
let navigator = &graph_editor.model.navigator;
let zoom_on_center = |amount: f32| ZoomEvent { focus: Vector2(0.0, 0.0), amount };
let zoom_duration_ms = Duration::from_millis(1000);
// Without debug mode
navigator.emit_zoom_event(zoom_on_center(-1.0));
sleep(zoom_duration_ms).await;
assert_abs_diff_eq!(camera.zoom(), 1.0, epsilon = 0.001);
navigator.emit_zoom_event(zoom_on_center(1.0));
sleep(zoom_duration_ms).await;
assert!(camera.zoom() < 1.0, "Camera zoom {} must be less than 1.0", camera.zoom());
navigator.emit_zoom_event(zoom_on_center(-2.0));
sleep(zoom_duration_ms).await;
assert_abs_diff_eq!(camera.zoom(), 1.0, epsilon = 0.001);
// With debug mode
project.enable_debug_mode();
navigator.emit_zoom_event(zoom_on_center(-1.0));
sleep(zoom_duration_ms).await;
assert!(camera.zoom() > 1.0, "Camera zoom {} must be greater than 1.0", camera.zoom());
navigator.emit_zoom_event(zoom_on_center(5.0));
sleep(zoom_duration_ms).await;
assert!(camera.zoom() < 1.0, "Camera zoom {} must be less than 1.0", camera.zoom());
navigator.emit_zoom_event(zoom_on_center(-5.0));
sleep(zoom_duration_ms).await;
assert!(camera.zoom() > 1.0, "Camera zoom {} must be greater than 1.0", camera.zoom());
}
#[wasm_bindgen_test]
async fn adding_node_with_add_node_button() {
const INITIAL_NODE_COUNT: usize = 2;
let test = Fixture::setup_new_project().await;
let graph_editor = test.graph_editor();
let scene = &test.ide.ensogl_app.display.default_scene;
let InitialNodes { below: (_, bottom_node), .. } =
InitialNodes::obtain_from_graph_editor(&graph_editor);
let bottom_node_pos = bottom_node.position();
// Node is created below the bottom-most one.
let (first_node_id, node_source, first_node) =
add_node_with_add_node_button(&graph_editor, "1 + 1").await;
assert!(node_source.is_none());
assert_eq!(graph_editor.model.nodes.len(), INITIAL_NODE_COUNT + 1);
let node_position = first_node.position();
assert!(
first_node.position().y < bottom_node_pos.y,
"Expected that {node_position}.y < {bottom_node_pos}.y"
);
// Selected node is used as a `source` node.
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(first_node_id);
let (_, node_source, _) = add_node_with_add_node_button(&graph_editor, "+ 1").await;
assert_eq!(node_source, Some(NodeSource { node: first_node_id }));
assert_eq!(graph_editor.model.nodes.len(), INITIAL_NODE_COUNT + 2);
// If there is a free space, the new node is created in the center of screen.
let camera = scene.layers.main.camera();
camera.update_xy(|pos| pos + Vector2(1000.0, 1000.0));
wait_a_frame().await;
graph_editor.model.nodes.deselect_all();
let (node_id, node_source, _) = add_node_with_add_node_button(&graph_editor, "1").await;
assert!(node_source.is_none());
assert_eq!(graph_editor.model.nodes.len(), INITIAL_NODE_COUNT + 3);
let node_position = graph_editor.model.get_node_position(node_id).expect(
"Node was not
added",
);
let center_of_screen = scene.screen_to_scene_coordinates(Vector3::zeros());
assert_abs_diff_eq!(node_position.x, center_of_screen.x, epsilon = 10.0);
assert_abs_diff_eq!(node_position.y, center_of_screen.y, epsilon = 10.0);
}
#[wasm_bindgen_test]
async fn adding_node_by_clicking_on_the_output_port() {
let test = Fixture::setup_new_project().await;
let graph_editor = test.graph_editor();
let (node_1_id, _, node_1) = add_node_with_internal_api(&graph_editor, "1 + 1").await;
let method = |editor: &GraphEditor| {
let port = node_1.model().output_port_shape().expect("No output port");
port.events_deprecated.mouse_over.emit(());
editor.start_node_creation_from_port();
};
let (_, source, node_2) = add_node(&graph_editor, "+ 1", method).await;
assert_eq!(source.unwrap(), NodeSource { node: node_1_id });
assert!(node_2.position().y < node_1.position().y);
}
#[wasm_bindgen_test]
async fn new_nodes_placement_with_nodes_selected() {
let test = Fixture::setup_new_project().await;
let graph_editor = test.graph_editor();
let InitialNodes { above: (node_1_id, node_1), below: (node_2_id, node_2) } =
InitialNodes::obtain_from_graph_editor(&graph_editor);
// Scenario 1. Creating a new node with one node selected.
graph_editor.model.nodes.select(node_2_id);
let (node_3_id, _, node_3) = add_node_with_add_node_button(&graph_editor, "+ 1").await;
assert_eq!(
node_3.position().x,
node_2.position().x,
"New node is not left-aligned to the selected one."
);
assert!(node_3.position().y < node_2.position().y, "New node is not below the selected one.");
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(node_2_id);
let (node_4_id, _, node_4) = add_node_with_shortcut(&graph_editor, "+ 1").await;
assert_eq!(
node_4.position().y,
node_3.position().y,
"New node is not vertically aligned to the previous one."
);
assert!(
node_4.position().x < node_3.position().x,
"New node is not to the left of the previous one."
);
graph_editor.remove_node(node_3_id);
graph_editor.remove_node(node_4_id);
graph_editor.model.nodes.deselect_all();
// Scenario 2. Creating a new node with multiple nodes selected.
node_1.set_position(Vector3(-100.0, 0.0, 0.0));
wait_a_frame().await;
graph_editor.model.nodes.select(node_1_id);
graph_editor.model.nodes.select(node_2_id);
let (.., node_5) = add_node_with_shortcut(&graph_editor, "+ 1").await;
assert_eq!(
node_5.position().x,
node_1.position().x,
"New node is not left-aligned to the first selected one."
);
assert!(node_5.position().y < node_2.position().y, "New node is not below the bottom node.");
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(node_1_id);
graph_editor.model.nodes.select(node_2_id);
let (node_6_id, _, node_6) = add_node_with_shortcut(&graph_editor, "+ 1").await;
assert_eq!(
node_6.position().y,
node_5.position().y,
"New node is not vertically aligned to the previous one."
);
assert!(
node_6.position().x < node_5.position().x,
"New node is not to the left of the previous one."
);
// Scenario 3. Creating a new node with enabled visualization.
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(node_6_id);
let (node_7_id, _, node_7) = add_node_with_shortcut(&graph_editor, "+ 1").await;
let pos_without_visualization = node_7.position().y;
graph_editor.remove_node(node_7_id);
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(node_6_id);
node_6.enable_visualization();
wait_a_frame().await;
let (.., node_7) = add_node_with_shortcut(&graph_editor, "+ 1").await;
assert!(
node_7.position().y < pos_without_visualization,
"New node is not below the visualization."
);
}
#[wasm_bindgen_test]
async fn mouse_oriented_node_placement() {
struct Case {
scene: Scene,
graph_editor: GraphEditor,
source_node: Node,
mouse_position: Vector2,
expected_position: Vector2,
}
impl Case {
fn run(&self) {
self.check_tab_key();
self.check_edge_drop();
}
fn check_searcher_opening_place(
&self,
added_node: FutureEvent<(NodeId, Option<NodeSource>, bool)>,
) {
let (new_node_id, _, _) = added_node.expect();
let new_node_pos =
self.graph_editor.model.get_node_position(new_node_id).map(|v| v.xy());
assert_eq!(new_node_pos, Some(self.expected_position));
self.graph_editor.stop_editing();
assert_eq!(self.graph_editor.model.nodes.len(), 2);
}
fn check_tab_key(&self) {
self.scene.mouse.frp_deprecated.position.emit(self.mouse_position);
let added_node = self.graph_editor.node_added.next_event();
self.graph_editor.start_node_creation();
self.check_searcher_opening_place(added_node);
}
fn check_edge_drop(&self) {
let port = self.source_node.view.model().output_port_shape().unwrap();
port.events_deprecated.emit_mouse_down(PrimaryButton);
port.events_deprecated.emit_mouse_up(PrimaryButton);
self.scene.mouse.frp_deprecated.position.emit(self.mouse_position);
assert!(
self.graph_editor.has_detached_edge.value(),
"No detached edge after clicking port"
);
let added_node = self.graph_editor.node_added.next_event();
self.scene.mouse.click_on_background(Vector2::zero());
enso_web::simulate_sleep((enso_shortcuts::DOUBLE_EVENT_TIME_MS + 10.0) as f64);
self.check_searcher_opening_place(added_node);
}
}
let test = Fixture::setup_new_project().await;
let scene = &test.ide.ensogl_app.display.default_scene;
let graph_editor = test.graph_editor();
let gap_x = graph_editor.default_x_gap_between_nodes.value();
let gap_y = graph_editor.default_y_gap_between_nodes.value();
let min_spacing = graph_editor.min_x_spacing_for_new_nodes.value();
let InitialNodes { above: (_, above), below: (_, below) } =
InitialNodes::obtain_from_graph_editor(&graph_editor);
let create_case =
|source_node: &Node, mouse_position: Vector2, expected_position: Vector2| Case {
scene: scene.clone_ref(),
graph_editor: graph_editor.clone_ref(),
source_node: source_node.clone_ref(),
mouse_position,
expected_position,
};
let far_away = below.position().xy() + Vector2(500.0, 500.0);
let far_away_expect = far_away;
create_case(&below, far_away, far_away_expect).run();
let under_below = below.position().xy() + Vector2(30.0, -25.0);
let under_below_expect = below.position().xy() + Vector2(0.0, -gap_y - node_view::HEIGHT);
create_case(&below, under_below, under_below_expect).run();
let under_above = above.position().xy() + Vector2(30.0, -25.0);
let under_above_expect = Vector2(
below.position().x - gap_x - min_spacing,
above.position().y - gap_y - node_view::HEIGHT,
);
create_case(&above, under_above, under_above_expect).run();
}