diff --git a/Makefile.toml b/Makefile.toml index 8ecbe6b2e..6f331b98f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -42,10 +42,10 @@ private = true [tasks.test] dependencies = ["build"] command = "cargo" -args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router"] +args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"] private = true [tasks.test-with-browser] -env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router"] } +env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] } private = true workspace = true diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 8b41019e1..092676322 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -57,4 +57,17 @@ hot-reload = ["dioxus-hot-reload"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } dioxus-hooks = { path = "../hooks" } -# image = "0.24.0" # enable this when generating a new desktop image +dioxus = { path = "../dioxus" } +exitcode = "1.1.2" +scraper = "0.16.0" + +# These tests need to be run on the main thread, so they cannot use rust's test harness. +[[test]] +name = "check_events" +path = "headless_tests/events.rs" +harness = false + +[[test]] +name = "check_rendering" +path = "headless_tests/rendering.rs" +harness = false diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs new file mode 100644 index 000000000..0281969ec --- /dev/null +++ b/packages/desktop/headless_tests/events.rs @@ -0,0 +1,351 @@ +use dioxus::html::geometry::euclid::Vector3D; +use dioxus::prelude::*; +use dioxus_desktop::DesktopContext; + +pub(crate) fn check_app_exits(app: Component) { + use dioxus_desktop::tao::window::WindowBuilder; + use dioxus_desktop::Config; + // This is a deadman's switch to ensure that the app exits + let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let should_panic_clone = should_panic.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(100)); + if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { + std::process::exit(exitcode::SOFTWARE); + } + }); + + dioxus_desktop::launch_cfg( + app, + Config::new().with_window(WindowBuilder::new().with_visible(false)), + ); + + should_panic.store(false, std::sync::atomic::Ordering::SeqCst); +} + +pub fn main() { + check_app_exits(app); +} + +fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) { + use_effect(cx, (), |_| { + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + async move { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + desktop_context.eval(&format!( + r#"let element = document.getElementById('{}'); + // Dispatch a synthetic event + const event = {}; + console.log(element, event); + element.dispatchEvent(event); + "#, + id, value + )); + } + }); +} + +#[allow(deprecated)] +fn app(cx: Scope) -> Element { + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + let recieved_events = use_state(cx, || 0); + + // button + mock_event( + cx, + "button", + r#"new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + button: 0, + })"#, + ); + // mouse_move_div + mock_event( + cx, + "mouse_move_div", + r#"new MouseEvent("mousemove", { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + })"#, + ); + // mouse_click_div + mock_event( + cx, + "mouse_click_div", + r#"new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + button: 2, + })"#, + ); + // mouse_dblclick_div + mock_event( + cx, + "mouse_dblclick_div", + r#"new MouseEvent("dblclick", { + view: window, + bubbles: true, + cancelable: true, + buttons: 1|2, + button: 2, + })"#, + ); + // mouse_down_div + mock_event( + cx, + "mouse_down_div", + r#"new MouseEvent("mousedown", { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + button: 2, + })"#, + ); + // mouse_up_div + mock_event( + cx, + "mouse_up_div", + r#"new MouseEvent("mouseup", { + view: window, + bubbles: true, + cancelable: true, + buttons: 0, + button: 0, + })"#, + ); + // wheel_div + mock_event( + cx, + "wheel_div", + r#"new WheelEvent("wheel", { + view: window, + deltaX: 1.0, + deltaY: 2.0, + deltaZ: 3.0, + deltaMode: 0x00, + bubbles: true, + })"#, + ); + // key_down_div + mock_event( + cx, + "key_down_div", + r#"new KeyboardEvent("keydown", { + key: "a", + code: "KeyA", + location: 0, + repeat: true, + keyCode: 65, + charCode: 97, + char: "a", + charCode: 0, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + isComposing: false, + which: 65, + bubbles: true, + })"#, + ); + // key_up_div + mock_event( + cx, + "key_up_div", + r#"new KeyboardEvent("keyup", { + key: "a", + code: "KeyA", + location: 0, + repeat: false, + keyCode: 65, + charCode: 97, + char: "a", + charCode: 0, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + isComposing: false, + which: 65, + bubbles: true, + })"#, + ); + // key_press_div + mock_event( + cx, + "key_press_div", + r#"new KeyboardEvent("keypress", { + key: "a", + code: "KeyA", + location: 0, + repeat: false, + keyCode: 65, + charCode: 97, + char: "a", + charCode: 0, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + isComposing: false, + which: 65, + bubbles: true, + })"#, + ); + // focus_in_div + mock_event( + cx, + "focus_in_div", + r#"new FocusEvent("focusin", {bubbles: true})"#, + ); + // focus_out_div + mock_event( + cx, + "focus_out_div", + r#"new FocusEvent("focusout",{bubbles: true})"#, + ); + + if **recieved_events == 12 { + println!("all events recieved"); + desktop_context.close(); + } + + cx.render(rsx! { + div { + button { + id: "button", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + recieved_events.modify(|x| *x + 1) + }, + } + div { + id: "mouse_move_div", + onmousemove: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + }, + } + div { + id: "mouse_click_div", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + }, + } + div{ + id: "mouse_dblclick_div", + ondblclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "mouse_down_div", + onmousedown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "mouse_up_div", + onmouseup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "wheel_div", + width: "100px", + height: "100px", + background_color: "red", + onwheel: move |event| { + println!("{:?}", event.data); + let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{ + panic!("Expected delta to be in pixels") + }; + assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); + recieved_events.modify(|x| *x + 1) + } + } + input{ + id: "key_down_div", + onkeydown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location, 0); + assert!(event.data.is_auto_repeating()); + + recieved_events.modify(|x| *x + 1) + } + } + input{ + id: "key_up_div", + onkeyup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location, 0); + assert!(!event.data.is_auto_repeating()); + + recieved_events.modify(|x| *x + 1) + } + } + input{ + id: "key_press_div", + onkeypress: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location, 0); + assert!(!event.data.is_auto_repeating()); + + recieved_events.modify(|x| *x + 1) + } + } + input{ + id: "focus_in_div", + onfocusin: move |event| { + println!("{:?}", event.data); + recieved_events.modify(|x| *x + 1) + } + } + input{ + id: "focus_out_div", + onfocusout: move |event| { + println!("{:?}", event.data); + recieved_events.modify(|x| *x + 1) + } + } + } + }) +} diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs new file mode 100644 index 000000000..635da9c23 --- /dev/null +++ b/packages/desktop/headless_tests/rendering.rs @@ -0,0 +1,94 @@ +use dioxus::prelude::*; +use dioxus_desktop::DesktopContext; + +pub(crate) fn check_app_exits(app: Component) { + use dioxus_desktop::tao::window::WindowBuilder; + use dioxus_desktop::Config; + // This is a deadman's switch to ensure that the app exits + let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let should_panic_clone = should_panic.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(100)); + if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { + std::process::exit(exitcode::SOFTWARE); + } + }); + + dioxus_desktop::launch_cfg( + app, + Config::new().with_window(WindowBuilder::new().with_visible(false)), + ); + + should_panic.store(false, std::sync::atomic::Ordering::SeqCst); +} + +fn main() { + check_app_exits(check_html_renders); +} + +fn use_inner_html(cx: &ScopeState, id: &'static str) -> Option { + let value: &UseRef> = use_ref(cx, || None); + use_effect(cx, (), |_| { + to_owned![value]; + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + async move { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + let html = desktop_context + .eval(&format!( + r#"let element = document.getElementById('{}'); + return element.innerHTML;"#, + id + )) + .await; + if let Ok(serde_json::Value::String(html)) = html { + println!("html: {}", html); + value.set(Some(html)); + } + } + }); + value.read().clone() +} + +const EXPECTED_HTML: &str = r#"

text

hello world

"#; + +fn check_html_renders(cx: Scope) -> Element { + let inner_html = use_inner_html(cx, "main_div"); + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + + if let Some(raw_html) = inner_html.as_deref() { + let fragment = scraper::Html::parse_fragment(raw_html); + println!("fragment: {:?}", fragment.html()); + let expected = scraper::Html::parse_fragment(EXPECTED_HTML); + println!("fragment: {:?}", expected.html()); + if fragment == expected { + println!("html matches"); + desktop_context.close(); + } + } + + let dyn_value = 0; + let dyn_element = rsx! { + div { + dangerous_inner_html: "

hello world

", + } + }; + + render! { + div { + id: "main_div", + div { + width: "100px", + height: "100px", + color: "rgb({dyn_value}, {dyn_value}, {dyn_value})", + id: 5, + input { + "type": "checkbox", + }, + h1 { + "text" + } + dyn_element + } + } + } +}