enso/integration-test/tests/graph_editor.rs
Paweł Grabarz bb39eeb12f
New node design (#7311)
Fixes #6552
Fixes #6910
Fixes #6872

Implementation of new node design. Includes many changes related to stylesheet update handling and per-style FRP construction, as well as refactoring of scene layers used by graph editor. Some additional components were migrated to use `Rectangle` shape and new mouse handling events. Fixed text rendering, where random thin lines appeared at the borders of glyph sprites. Refined edge layout to match new node sizes and not leave any visible gaps between line segments.

The node colors are currently randomly selected from predefined list. Later this will be improved to use group information from the suggestion database, once that is fully migrated to use the documentation tags, thus removing the dependency on the execution context.


https://github.com/enso-org/enso/assets/919491/aa687e53-a2fa-4e95-a15f-132c05e6337a


<img width="653" alt="image" src="https://github.com/enso-org/enso/assets/919491/30f3e897-62fc-40ea-b57b-124ac923bafd">
2023-07-27 13:00:47 +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_hover_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_hover_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();
}