From 9d6f9373f9b18848b8fb1a937f450ccff1954539 Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Tue, 22 Feb 2022 17:43:37 +0100 Subject: [PATCH] Add tests for debug mode and zoom restriction. (#3289) This PR adds integration tests created during acceptance process of [181181159](https://www.pivotaltracker.com/story/show/181181159) and [181181203](https://www.pivotaltracker.com/story/show/181181203). The PRs for those tasks were merged, because I hadn't realized they should not. Additionally, as the `wasm-bindgen` version was bumped, I extended the timeout of integration tests and made them headless. --- Cargo.lock | 10 ++ app/gui/docs/CONTRIBUTING.md | 14 ++- app/gui/src/lib.rs | 1 + app/gui/view/graph-editor/src/lib.rs | 2 +- app/gui/view/src/debug_mode_popup.rs | 10 +- app/gui/view/src/project.rs | 5 + build/run.js | 9 +- integration-test/Cargo.toml | 5 +- integration-test/src/lib.rs | 78 ++++++++++++++- integration-test/tests/graph_editor.rs | 97 ++++++++++++++++--- .../core/src/display/navigation/navigator.rs | 32 +++--- .../display/navigation/navigator/events.rs | 12 +++ 12 files changed, 227 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 910bc709d85..798ea2eef25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "assert_approx_eq" version = "1.1.0" @@ -1016,6 +1025,7 @@ dependencies = [ name = "enso-integration-test" version = "0.1.0" dependencies = [ + "approx 0.5.1", "enso-frp", "enso-gui", "enso-prelude", diff --git a/app/gui/docs/CONTRIBUTING.md b/app/gui/docs/CONTRIBUTING.md index f189693f0d8..0dcb489a17d 100644 --- a/app/gui/docs/CONTRIBUTING.md +++ b/app/gui/docs/CONTRIBUTING.md @@ -281,18 +281,16 @@ have prepared several scripts which maximally automate the process: is in your `PATH`. - **Integration Tests** The integration tests are gathered in `integration-test` - crate. One test suite can be run with - `node ./run integration-test -- --test `. This will spawn required - Engine process and then set up a server on localhost:8000 - open the page in - Chrome browser to see the tests running. The `` is a name of the - file in `integration-test/tests` directory without extension, for example - `graph_editor`. + crate. You can run them with `node ./run integration-test` command. The script + will spawn required Engine process. + - To run une test suite add `-- --test ` at end of command + options. The `` is a name of the file in + `integration-test/tests` directory without extension, for example + `graph_editor`. - The integration test can create and leave new Enso projects. **Keep it in mind when running the script with your own backend (the `--no-backend` option)**. The Engine spawned by the script will use a dedicated workspace created in temporary directory, so the user workspace will not be affected. - - Note: in the future there will be possibility to run all tests suite - headlessly. - **Linting** Please be sure to fix all errors reported by `node ./run lint` before creating a pull request to this repository. diff --git a/app/gui/src/lib.rs b/app/gui/src/lib.rs index d821b56fa2b..3379f75cbd5 100644 --- a/app/gui/src/lib.rs +++ b/app/gui/src/lib.rs @@ -65,6 +65,7 @@ pub mod test; pub mod transport; pub use crate::ide::*; +pub use ide_view as view; use ensogl::system::web; use wasm_bindgen::prelude::*; diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index fd425c48298..dfa56a050c5 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -1454,6 +1454,7 @@ pub struct GraphEditorModel { pub edges: Edges, pub vis_registry: visualization::Registry, pub drop_manager: ensogl_drop_manager::Manager, + pub navigator: Navigator, // FIXME[MM]: The tooltip should live next to the cursor in `Application`. This does not // currently work, however, because the `Application` lives in enso-core, and the tooltip // requires enso-text, which in turn depends on enso-core, creating a cyclic dependency. @@ -1461,7 +1462,6 @@ pub struct GraphEditorModel { touch_state: TouchState, visualisations: Visualisations, frp: FrpEndpoints, - navigator: Navigator, profiling_statuses: profiling::Statuses, profiling_button: component::profiling::Button, styles_frp: StyleWatchFrp, diff --git a/app/gui/view/src/debug_mode_popup.rs b/app/gui/view/src/debug_mode_popup.rs index 22265b012cc..5c42d8537c7 100644 --- a/app/gui/view/src/debug_mode_popup.rs +++ b/app/gui/view/src/debug_mode_popup.rs @@ -41,11 +41,12 @@ const LABEL_PADDING_TOP: f32 = 50.0; /// Text label that disappears after a predefined delay. #[derive(Debug, Clone, CloneRef)] -struct PopupLabel { +pub struct PopupLabel { label: Label, network: frp::Network, delay_animation: DelayedAnimation, - show: frp::Source, + /// Show the Popup with the given message. + pub show: frp::Source, } impl display::Object for PopupLabel { @@ -189,6 +190,11 @@ impl View { Self { frp, model } } + + /// Get the label of the popup. + pub fn label(&self) -> &PopupLabel { + &self.model.label + } } impl display::Object for View { diff --git a/app/gui/view/src/project.rs b/app/gui/view/src/project.rs index a50b5673d66..a002baf6f30 100644 --- a/app/gui/view/src/project.rs +++ b/app/gui/view/src/project.rs @@ -676,6 +676,11 @@ impl View { pub fn open_dialog(&self) -> &OpenDialog { &self.model.open_dialog } + + /// Debug Mode Popup + pub fn debug_mode_popup(&self) -> &debug_mode_popup::View { + &self.model.debug_mode_popup + } } impl display::Object for View { diff --git a/build/run.js b/build/run.js index 466557b3bf3..e701b9dc52e 100755 --- a/build/run.js +++ b/build/run.js @@ -268,7 +268,14 @@ commands['integration-test'].rust = async function (argv) { } try { console.log(`Running Rust WASM test suite.`) - let args = ['test', '--chrome', 'integration-test', '--profile=integration-test'] + process.env.WASM_BINDGEN_TEST_TIMEOUT = 120 + let args = [ + 'test', + '--headless', + '--chrome', + 'integration-test', + '--profile=integration-test', + ] await run_cargo('wasm-pack', args) } finally { console.log(`Shutting down Project Manager`) diff --git a/integration-test/Cargo.toml b/integration-test/Cargo.toml index 1187447d938..b6c8012e6f5 100644 --- a/integration-test/Cargo.toml +++ b/integration-test/Cargo.toml @@ -4,12 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] +approx = "0.5.1" ensogl = { path = "../lib/rust/ensogl" } enso-frp = { path = "../lib/rust/frp" } enso-prelude = { path = "../lib/rust/prelude" } enso-gui = { path = "../app/gui" } enso-web = { path = "../lib/rust/web" } -wasm-bindgen = { version = "0.2.58" } - -[dev-dependencies] +wasm-bindgen = { version = "0.2.78" } wasm-bindgen-test = "0.3.8" diff --git a/integration-test/src/lib.rs b/integration-test/src/lib.rs index 264c26ededa..cd2828d2e3c 100644 --- a/integration-test/src/lib.rs +++ b/integration-test/src/lib.rs @@ -15,16 +15,26 @@ #![warn(missing_copy_implementations)] #![warn(missing_debug_implementations)] -pub use enso_prelude as prelude; - -use enso_prelude::*; +use crate::prelude::*; +use enso_frp::future::EventOutputExt; use enso_gui::executor::web::EventLoopExecutor; use enso_gui::initializer::setup_global_executor; use enso_gui::Ide; use enso_web::HtmlDivElement; use enso_web::NodeInserter; use enso_web::StyleSetter; +use ensogl::application::Application; + +/// Reexports of commonly-used structures, methods and traits. +pub mod prelude { + pub use crate::IntegrationTest; + pub use crate::IntegrationTestOnNewProject; + + pub use enso_frp::future::EventOutputExt; + pub use enso_gui::prelude::*; + pub use wasm_bindgen_test::wasm_bindgen_test; +} @@ -43,7 +53,9 @@ pub struct IntegrationTest { } impl IntegrationTest { - /// Initializes the executor and `Ide` structure and returns new Fixture + const SCREEN_SIZE: (f32, f32) = (1920.0, 1000.0); + + /// Initializes the executor and `Ide` structure and returns new Fixture. pub async fn setup() -> Self { enso_web::forward_panic_hook_to_error(); let executor = setup_global_executor(); @@ -54,8 +66,16 @@ impl IntegrationTest { let initializer = enso_gui::ide::Initializer::new(default()); let ide = initializer.start().await.expect("Failed to initialize the application."); + Self::set_screen_size(&ide.ensogl_app); Self { executor, ide, root_div } } + + fn set_screen_size(app: &Application) { + let (screen_width, screen_height) = Self::SCREEN_SIZE; + app.display.scene().layers.iter_sublayers_and_masks_nested(|layer| { + layer.camera().set_screen(screen_width, screen_height) + }); + } } impl Drop for IntegrationTest { @@ -63,3 +83,53 @@ impl Drop for IntegrationTest { self.root_div.remove(); } } + + + +// =================================== +// === IntegrationTestOnNewProject === +// =================================== + +/// A fixture for IDE integration tests on created project. It is derived from [`IntegrationTest`]. +/// During setup, the Ide initialization is performed, then new project is created, and we wait till +/// the prompt for user will be displayed (thus informing us, that the project is ready to work). +#[derive(Debug)] +pub struct IntegrationTestOnNewProject { + parent: IntegrationTest, +} + +impl Deref for IntegrationTestOnNewProject { + type Target = IntegrationTest; + + fn deref(&self) -> &Self::Target { + &self.parent + } +} + +impl IntegrationTestOnNewProject { + /// Test initialization. After returning, the IDE is in state with new project opened and ready + /// to work (after libraries' compilation). + pub async fn setup() -> Self { + let parent = IntegrationTest::setup().await; + let ide = &parent.ide; + let project = ide.presenter.view().project(); + let controller = ide.presenter.controller(); + let project_management = + controller.manage_projects().expect("Cannot access Managing Project API"); + + let expect_prompt = project.show_prompt.next_event(); + project_management.create_new_project(None).await.expect("Failed to create new project"); + expect_prompt.await; + Self { parent } + } + + /// Get the Project View. + pub fn project_view(&self) -> enso_gui::view::project::View { + self.ide.presenter.view().project() + } + + /// Get the Graph Editor. + pub fn graph_editor(&self) -> enso_gui::view::graph_editor::GraphEditor { + self.project_view().graph().clone_ref() + } +} diff --git a/integration-test/tests/graph_editor.rs b/integration-test/tests/graph_editor.rs index e8a2670365d..b1535d74132 100644 --- a/integration-test/tests/graph_editor.rs +++ b/integration-test/tests/graph_editor.rs @@ -1,23 +1,17 @@ -use enso_frp::future::EventOutputExt; -use enso_integration_test::IntegrationTest; -use wasm_bindgen_test::wasm_bindgen_test; +use enso_integration_test::prelude::*; + +use approx::assert_abs_diff_eq; +use enso_web::sleep; +use ensogl::display::navigation::navigator::ZoomEvent; +use std::time::Duration; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn create_new_project_and_add_nodes() { - let test = IntegrationTest::setup().await; - let ide = &test.ide; - let project = ide.presenter.view().project(); - let graph_editor = project.graph(); - let controller = ide.presenter.controller(); - let project_management = - controller.manage_projects().expect("Cannot access Managing Project API"); - - let expect_prompt = project.show_prompt.next_event(); - project_management.create_new_project(None).await.expect("Failed to create new project"); - expect_prompt.await; + let test = IntegrationTestOnNewProject::setup().await; + let graph_editor = test.graph_editor(); assert_eq!(graph_editor.model.nodes.all.len(), 2); let expect_node_added = graph_editor.node_added.next_event(); @@ -29,3 +23,78 @@ async fn create_new_project_and_add_nodes() { graph_editor.model.nodes.get_cloned_ref(&added_node_id).expect("Added node is not added"); assert_eq!(added_node.view.expression.value().to_string(), ""); } + +#[wasm_bindgen_test] +async fn debug_mode() { + let test = IntegrationTestOnNewProject::setup().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().label().show.next_event(); + project.enable_debug_mode(); + assert!(expect_mode.expect()); + let message = expect_popup_message.expect(); + assert!( + message.contains("Debug Mode enabled"), + "Message \"{}\" does not mention enabling Debug mode", + message + ); + assert!( + message.contains(enso_gui::view::debug_mode_popup::DEBUG_MODE_SHORTCUT), + "Message \"{}\" does not inform about shortcut to turn mode off", + message + ); + assert!(graph_editor.debug_mode.value()); + + // Turning Off + let expect_mode = project.debug_mode.next_event(); + let expect_popup_message = project.debug_mode_popup().label().show.next_event(); + project.disable_debug_mode(); + assert!(!expect_mode.expect()); + let message = expect_popup_message.expect(); + assert!( + message.contains("Debug Mode disabled"), + "Message \"{}\" does not mention disabling of debug mode", + message + ); + assert!(!graph_editor.debug_mode.value()); +} + +#[wasm_bindgen_test] +async fn zooming() { + let test = IntegrationTestOnNewProject::setup().await; + let project = test.project_view(); + let graph_editor = test.graph_editor(); + let camera = test.ide.ensogl_app.display.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()); +} diff --git a/lib/rust/ensogl/core/src/display/navigation/navigator.rs b/lib/rust/ensogl/core/src/display/navigation/navigator.rs index aa3011986f4..8b7eb838776 100644 --- a/lib/rust/ensogl/core/src/display/navigation/navigator.rs +++ b/lib/rust/ensogl/core/src/display/navigation/navigator.rs @@ -1,17 +1,17 @@ mod events; +pub use crate::display::navigation::navigator::events::PanEvent; +pub use crate::display::navigation::navigator::events::ZoomEvent; + use crate::prelude::*; use crate::animation::physics; use crate::control::callback; use crate::display::camera::Camera2d; +use crate::display::navigation::navigator::events::NavigatorEvents; use crate::display::object::traits::*; use crate::display::Scene; -use events::NavigatorEvents; -use events::PanEvent; -use events::ZoomEvent; - // ================= @@ -33,7 +33,7 @@ const MIN_ZOOM: f32 = 0.001; /// Navigator enables camera navigation with mouse interactions. #[derive(Debug)] pub struct NavigatorModel { - _events: NavigatorEvents, + events: NavigatorEvents, simulator: physics::inertia::DynSimulator, resize_callback: callback::Handle, zoom_speed: SharedSwitch, @@ -50,7 +50,7 @@ impl NavigatorModel { let pan_speed = Rc::new(Cell::new(Switch::On(1.0))); let disable_events = Rc::new(Cell::new(true)); let max_zoom: Rc> = default(); - let (simulator, resize_callback, _events) = Self::start_navigator_events( + let (simulator, resize_callback, events) = Self::start_navigator_events( scene, camera, zoom_speed.clone_ref(), @@ -58,15 +58,7 @@ impl NavigatorModel { disable_events.clone_ref(), max_zoom.clone_ref(), ); - Self { - _events, - simulator, - resize_callback, - zoom_speed, - pan_speed, - disable_events, - max_zoom, - } + Self { events, simulator, resize_callback, zoom_speed, pan_speed, disable_events, max_zoom } } fn create_simulator(camera: &Camera2d) -> physics::inertia::DynSimulator { @@ -180,6 +172,16 @@ impl NavigatorModel { pub fn set_max_zoom(&self, value: Option) { self.max_zoom.set(value); } + + /// Emit zoom event. This function could be used in the tests to simulate user interactions. + pub fn emit_zoom_event(&self, event: ZoomEvent) { + self.events.emit_zoom_event(event); + } + + /// Emit pan event. This function could be used in the tests to simulate user interactions. + pub fn emit_pan_event(&self, event: PanEvent) { + self.events.emit_pan_event(event); + } } diff --git a/lib/rust/ensogl/core/src/display/navigation/navigator/events.rs b/lib/rust/ensogl/core/src/display/navigation/navigator/events.rs index 7addc7de203..3efc904fb15 100644 --- a/lib/rust/ensogl/core/src/display/navigation/navigator/events.rs +++ b/lib/rust/ensogl/core/src/display/navigation/navigator/events.rs @@ -17,6 +17,7 @@ use nalgebra::Vector2; pub trait FnZoomEvent = FnMut(ZoomEvent) + 'static; /// A struct holding zoom event information, such as the focus point and the amount of zoom. +#[derive(Clone, Copy, Debug, Default)] pub struct ZoomEvent { pub focus: Vector2, pub amount: f32, @@ -45,6 +46,7 @@ impl ZoomEvent { pub trait FnPanEvent = FnMut(PanEvent) + 'static; /// A struct holding pan event information. +#[derive(Clone, Copy, Debug, Default)] pub struct PanEvent { pub movement: Vector2, } @@ -348,6 +350,16 @@ impl NavigatorEvents { }); self.mouse_move = Some(listener); } + + /// Emit zoom event. This function could be used in the tests to simulate user interactions. + pub fn emit_zoom_event(&self, event: ZoomEvent) { + self.data.on_zoom(event); + } + + /// Emit pan event. This function could be used in the tests to simulate user interactions. + pub fn emit_pan_event(&self, event: PanEvent) { + self.data.on_pan(event); + } } fn movement_to_zoom(v: Vector2) -> f32 {