diff --git a/.gitignore b/.gitignore index 1af73ecd..83535c64 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ test_db **/.DS_Store explorer.log .gitaipconfig + +# Ignore egui kittest snapshots +tests/snapshots/ diff --git a/Cargo.lock b/Cargo.lock index 95610fe0..41ecad89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1196,7 +1196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "termcolor", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1205,6 +1205,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.52.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -1835,6 +1845,19 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" +[[package]] +name = "dify" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11217d469eafa3b809ad84651eb9797ccbb440b4a916d5d85cb1b994e89787f6" +dependencies = [ + "anyhow", + "colored", + "getopts", + "image", + "rayon", +] + [[package]] name = "digest" version = "0.10.7" @@ -2172,6 +2195,7 @@ dependencies = [ "objc2-foundation 0.2.2", "parking_lot", "percent-encoding", + "pollster", "profiling", "raw-window-handle", "ron", @@ -2181,6 +2205,7 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "web-time", + "wgpu", "winapi", "windows-sys 0.59.0", "winit", @@ -2307,9 +2332,14 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c46def610cf9486675aeec698d4e36a949ec0b0e1f6096135b0584dcfd52aa47" dependencies = [ + "dify", "eframe", "egui", + "egui-wgpu", + "image", "kittest", + "pollster", + "wgpu", ] [[package]] @@ -2862,6 +2892,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getopts" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +dependencies = [ + "unicode-width 0.2.1", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -3029,6 +3068,18 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows", +] + [[package]] name = "gpu-descriptor" version = "0.3.2" @@ -5225,6 +5276,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "prettyplease" version = "0.2.34" @@ -5449,6 +5506,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "range-alloc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" + [[package]] name = "raw-cpuid" version = "11.5.0" @@ -7098,6 +7161,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -7600,6 +7669,7 @@ dependencies = [ "document-features", "js-sys", "log", + "naga", "parking_lot", "profiling", "raw-window-handle", @@ -7647,13 +7717,16 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", + "bit-set 0.8.0", "bitflags 2.9.1", + "block", "bytemuck", "cfg_aliases", "core-graphics-types", "glow", "glutin_wgl_sys", "gpu-alloc", + "gpu-allocator", "gpu-descriptor", "js-sys", "khronos-egl", @@ -7668,6 +7741,7 @@ dependencies = [ "ordered-float", "parking_lot", "profiling", + "range-alloc", "raw-window-handle", "renderdoc-sys", "rustc-hash 1.1.0", @@ -7677,6 +7751,7 @@ dependencies = [ "web-sys", "wgpu-types", "windows", + "windows-core 0.58.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d72689e7..59052bea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,4 +69,4 @@ raw-cpuid = "11.5.0" [dev-dependencies] tempfile = { version = "3.20.0" } -egui_kittest = { version = "0.31.1", features = ["eframe"] } +egui_kittest = { version = "0.31.1", features = ["eframe", "snapshot", "wgpu"] } diff --git a/src/logging.rs b/src/logging.rs index 6df7e4d5..7889abb2 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -24,7 +24,11 @@ pub fn initialize_logger() { // Set global subscriber with proper error handling if let Err(e) = tracing::subscriber::set_global_default(subscriber) { - panic!("Unable to set global default subscriber: {:?}", e); + // log to stderr and ignore the error + eprintln!( + "WARNING: cannot initialize logging, logging might not work correctly: {:?}", + e + ); } // Log panic events diff --git a/src/ui/components/left_panel.rs b/src/ui/components/left_panel.rs index f3f15a33..0474797d 100644 --- a/src/ui/components/left_panel.rs +++ b/src/ui/components/left_panel.rs @@ -1,6 +1,7 @@ use crate::app::AppAction; use crate::context::AppContext; use crate::ui::components::styled::GradientButton; +use crate::ui::components::test_label::TestableWidget; use crate::ui::theme::{DashColors, Shadow, Shape, Spacing}; use crate::ui::RootScreenType; use dash_sdk::dashcore_rpc::dashcore::Network; @@ -56,7 +57,8 @@ pub fn add_left_panel( let mut action = AppAction::None; // Define the button details directly in this function - let buttons = [ + // Contains (label, screen type, icon path) + let buttons: [(&'static str, RootScreenType, &'static str); 7] = [ ("I", RootScreenType::RootScreenIdentities, "identity.png"), ("Q", RootScreenType::RootScreenDocumentQuery, "doc.png"), ("O", RootScreenType::RootScreenMyTokenBalances, "tokens.png"), @@ -112,10 +114,13 @@ pub fn add_left_panel( // Add icon-based button if texture is loaded if let Some(ref texture) = texture { - let button = - ImageButton::new(texture).frame(false).tint(button_color); + let button = ImageButton::new(texture) + .frame(false) + .tint(button_color) + .test_label(label); let added = ui.add(button); + if added.clicked() { action = AppAction::SetMainScreenThenGoToMainScreen(*screen_type); diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index d987a778..75242836 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -4,6 +4,7 @@ pub mod entropy_grid; pub mod left_panel; pub mod left_wallet_panel; pub mod styled; +pub mod test_label; pub mod tokens_subscreen_chooser_panel; pub mod tools_subscreen_chooser_panel; pub mod top_panel; diff --git a/src/ui/components/styled.rs b/src/ui/components/styled.rs index a9f5ac01..42334433 100644 --- a/src/ui/components/styled.rs +++ b/src/ui/components/styled.rs @@ -6,7 +6,7 @@ use crate::{ }; use egui::{ Button, CentralPanel, Color32, Context, Frame, Margin, Response, RichText, Stroke, TextEdit, - Ui, Vec2, + Ui, Vec2, Widget, }; /// Styled button variants @@ -217,6 +217,12 @@ impl<'a> StyledCheckbox<'a> { } } +impl Widget for StyledCheckbox<'_> { + fn ui(self, ui: &mut Ui) -> Response { + self.show(ui) + } +} + /// Gradient button with animated effects pub(crate) struct GradientButton { text: String, diff --git a/src/ui/components/test_label.rs b/src/ui/components/test_label.rs new file mode 100644 index 00000000..7a5b68b6 --- /dev/null +++ b/src/ui/components/test_label.rs @@ -0,0 +1,60 @@ +use egui::{Sense, Widget, WidgetInfo}; + +/// A wrapper widget that adds a label to any `egui::Widget` for testing purposes. +/// +/// This widget allows you to attach a label to any widget, which can be used in tests +/// to identify and interact with the widget from egui_kittest. +/// Only use when there is no other way to identify the widget in tests. +/// +/// ## Example usage: +/// +/// ```rust +/// use egui::Widget; +/// use dash_evo_tool::ui::components::test_label::{TestLabel, TestableWidget}; +/// fn my_widget(ui: &mut egui::Ui) { +/// let my_button = egui::Button::new("Click me"); +/// ui.add(my_button.test_label("my_button")); +/// } +/// ``` +pub struct TestLabel { + pub label: String, + pub inner: T, +} + +impl TestLabel { + pub fn new(inner: T, label: &str) -> Self { + Self { + label: label.to_string(), + inner, + } + } +} + +impl egui::Widget for TestLabel { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + let scope = ui.scope(|ui| self.inner.ui(ui)); + let response = scope.response.interact(Sense::click()); + + response.widget_info(move || { + let label = self.label.clone(); + WidgetInfo::labeled(egui::WidgetType::Other, ui.is_enabled(), label) + }); + + // Pass all interactions from the inner widget + scope.inner.union(response) + } +} +/// Trait to allow any widget to be tested with a label +/// +/// This trait provides a method to attach a test label to any widget. +/// The label can be used to identify the widget in tests, making it easier to interact with +/// and assert conditions on the widget during testing. +pub trait TestableWidget { + fn test_label(self, label: &str) -> TestLabel; +} + +impl TestableWidget for T { + fn test_label(self, label: &str) -> TestLabel { + TestLabel::new(self, label) + } +} diff --git a/src/ui/network_chooser_screen.rs b/src/ui/network_chooser_screen.rs index ba9ce5f4..17baac2a 100644 --- a/src/ui/network_chooser_screen.rs +++ b/src/ui/network_chooser_screen.rs @@ -6,12 +6,14 @@ use crate::config::Config; use crate::context::AppContext; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::{island_central_panel, StyledCard, StyledCheckbox}; +use crate::ui::components::test_label::TestableWidget; use crate::ui::components::top_panel::add_top_panel; use crate::ui::theme::{DashColors, ThemeMode}; use crate::ui::{RootScreenType, ScreenLike}; use dash_sdk::dpp::dashcore::Network; use dash_sdk::dpp::identity::TimestampMillis; use eframe::egui::{self, Context, Ui}; +use egui::Widget; use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -538,7 +540,11 @@ impl NetworkChooserScreen { // Network selection let mut is_selected = self.current_network == network; - if StyledCheckbox::new(&mut is_selected, "").show(ui).clicked() && is_selected { + + let checkbox = StyledCheckbox::new(&mut is_selected, "") + .test_label(&format!("select_network_{}", network.magic())); + + if checkbox.ui(ui).clicked() && is_selected { self.current_network = network; app_action = AppAction::SwitchNetwork(network); // Recheck in 1 second diff --git a/tests/kittest/det_harness.rs b/tests/kittest/det_harness.rs new file mode 100644 index 00000000..cf409c58 --- /dev/null +++ b/tests/kittest/det_harness.rs @@ -0,0 +1,214 @@ +use dash_evo_tool::app::AppState; +use dash_sdk::dpp::dashcore::Network; +use egui::accesskit::Role; +use egui_kittest::{ + kittest::{Node, Queryable}, + Harness, +}; + +/// Test helper and tools for running egui tests of the Dash Evo Tool app +pub struct DETHarness<'a> { + pub kittest: Harness<'a, AppState>, + name: String, +} + +impl DETHarness<'_> { + /// Create a new test harness for the Dash Evo Tool app. + /// + /// `name` is used to identify the test and will be part of the snapshot file name. + pub fn new(name: &str) -> Self { + Self::setup_logging(); + + let harness = egui_kittest::Harness::builder() + .with_max_steps(100) + .build_eframe(|ctx| AppState::new(ctx.egui_ctx.clone()).with_animations(false)); + + let mut me = DETHarness { + kittest: harness, + name: name.to_string(), + }; + + // Set the window size for the test + // Fixme: find out how to scroll the window + me.kittest.set_size(egui::vec2(800.0, 3000.0)); + // Run one frame to ensure the app initializes + me.kittest.run(); + + me + } + + fn setup_logging() { + tracing_subscriber::fmt() + .with_env_filter("error, dash_evo_tool=debug,kittest=trace") + .init(); + } + + pub fn state(&self) -> &egui_kittest::kittest::State { + self.kittest.kittest_state() + } + + /// Execute a potentially panicking operation and continue execution + /// Takes a snapshot on panic and returns whether the operation succeeded + pub fn try_execute( + &mut self, + operation_name: &str, + operation: F, + ) -> Result> + where + F: FnOnce(&mut Self) -> R + std::panic::UnwindSafe, + { + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(5) + .enable_all() + .build() + .expect("Failed to create Tokio runtime"); + + let result = runtime.block_on(self.execute(operation_name, operation)); + runtime.shutdown_background(); + + // let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| operation(self))); + + result + } + + async fn execute( + &mut self, + operation_name: &str, + operation: F, + ) -> Result> + where + F: FnOnce(&mut Self) -> R + std::panic::UnwindSafe, + { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| operation(self))); + + match result { + Ok(value) => Ok(value), + Err(panic_info) => { + eprintln!( + "Operation '{}' panicked, taking snapshot...", + operation_name + ); + self.kittest + .snapshot(&format!("{}_{}_panic", self.name, operation_name)); + Err(panic_info) + } + } + } + + /// Connect to a selected network + pub fn connect_to_network(&mut self, network: Network) { + let index = match network { + Network::Dash => 0, + Network::Testnet => 1, + Network::Devnet => 2, + Network::Regtest => 3, + _ => panic!("Unsupported network"), + }; + self.click_by_label("N"); + self.run(); + + self.click_by_label(&format!("select_network_{}", network.magic())); + + // start dash-qt + let start = self + .kittest + .kittest_state() + .get_all_by_role_and_label(Role::Button, "Start") + .nth(index) + .expect("Button for the network not found"); + start.click(); + self.run(); + + // We need to wait for dash-qt to start and sync + std::thread::sleep(std::time::Duration::from_secs(6)); + + self.snapshot("network_connected"); + } +} + +impl<'a> DETHarness<'a> { + /// Run the test harness, executing all registered operations. + /// + /// See [Harness::run] for more details. + pub fn run(&mut self) { + self.kittest.run(); + } + /// Click a button by label + pub fn click_by_label(&mut self, label: &str) { + let btn = self.kittest.kittest_state().get_by_label(label); + btn.click(); + self.run(); + } + + /// Click a button by value + pub fn click_by_value(&mut self, value: &str) { + let btn = self.kittest.kittest_state().get_by_value(value); + btn.click(); + self.run(); + } + + /// Set text in a field by label + pub fn set_text_by_label(&mut self, label: &str, text: &str) { + let field = self.kittest.kittest_state().get_by_label(label); + field.type_text(text); + self.run(); + } + + /// Get a node by label + /// + /// ## Panics + /// + /// All get_ functions will panic if the node is not found. + pub fn get_by_label(&'a self, button: &'a str) -> Node<'a> { + self.kittest.kittest_state().get_by_label(button) + } + + /// Query all nodes by label + pub fn query_all_by_label(&'a self, label: &'a str) -> Vec> { + self.kittest + .kittest_state() + .query_all_by_label(label) + .collect() + } + + /// Take a snapshot + pub fn snapshot(&mut self, name: &str) { + self.kittest.snapshot(name); + } + /// Wait until label is present or timeout occurs + pub fn wait_all_by_label<'b>( + &'b mut self, + label: &'a str, + timeout: std::time::Duration, + ) -> Result>, String> { + let start = std::time::Instant::now(); + + while start.elapsed() < timeout { + self.kittest.run(); + + if !self.query_all_by_label(label).is_empty() { + break; + } + // let mut labels = self.kittest.kittest_state().query_all_by_label(label); + // if let Some(next) = labels.next() { + // tracing::trace!(?next, "Found label: {}", label); + // return Ok(next); + // } + // drop(labels); + + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + // query again to avoid borrow checker issues + let nodes = self.query_all_by_label(label); + if !nodes.is_empty() { + tracing::trace!(?nodes, "Found label: {}", label); + Ok(nodes) + } else { + Err(format!( + "Label '{}' not found after waiting for {:?}", + label, timeout + )) + } + } +} diff --git a/tests/kittest/identity.rs b/tests/kittest/identity.rs new file mode 100644 index 00000000..78c7ca8b --- /dev/null +++ b/tests/kittest/identity.rs @@ -0,0 +1,36 @@ +use dash_sdk::dpp::dashcore::Network; +use std::time::Duration; + +use crate::det_harness::DETHarness; + +/// When I go to the Identities screen and click on the "Create Identity" button, +/// When I fill in the form with valid data, +/// Then I should see a success message indicating that the identity was created successfully. +#[test] +fn test_create_identity() { + DETHarness::new("create_identity") + .try_execute("create_identity", |det| { + det.connect_to_network(Network::Testnet); + + det.click_by_label("I"); + det.click_by_label("Create Identity"); + // Ensure the instruction text is present + det.get_by_label("Follow these steps to create your identity!"); + + det.click_by_value("Select funding method"); + + // det.set_text_by_label("Identity Name", "Test Identity"); + // TODO: this does not work, we cannot click here + det.wait_all_by_label("Use Wallet Balance", Duration::from_secs(5)) + .expect("Wallet balance required"); + det.snapshot("identity_created"); + let success_nodes = det.query_all_by_label("Identity created successfully"); + tracing::debug!( + count = success_nodes.len(), + ?success_nodes, + "Created identity" + ); + assert!(!success_nodes.is_empty(), "Success message not found"); + }) + .expect("Failed to run create identity test"); +} diff --git a/tests/kittest/left_panel.rs b/tests/kittest/left_panel.rs new file mode 100644 index 00000000..3f39be0a --- /dev/null +++ b/tests/kittest/left_panel.rs @@ -0,0 +1,31 @@ +use crate::det_harness::DETHarness; + +/// Start the Dash Evo Tool app and click on all icons in the left panel +/// to ensure they are clickable and correct screens are shown. +#[test] +fn test_left_panel() { + // Create a test harness for the egui app + + DETHarness::new("left_panel_icon_clicks") + .try_execute("app_startup", |det| { + // label => text to find + let buttons = vec![ + ("I", "Identities"), + ("Q", "Contracts"), + ("O", "Tokens"), + ("C", "DPNS Subscreens"), + ("W", "Wallets"), + ("T", "Tools"), + ("N", "Networks"), + ]; + for (button, text) in buttons { + let icon = det.get_by_label(button); + icon.click(); + det.run(); + det.snapshot(&format!("app_startup.{}", button)); + let nodes: Vec<_> = det.query_all_by_label(text); + tracing::debug!(count = nodes.len(), ?nodes, "Clicked on icon: {}", button); + } + }) + .expect("Failed to run app startup test"); +} diff --git a/tests/kittest/main.rs b/tests/kittest/main.rs index 27a425da..df893c2c 100644 --- a/tests/kittest/main.rs +++ b/tests/kittest/main.rs @@ -1 +1,3 @@ -mod startup; +pub mod det_harness; +mod identity; +mod left_panel; diff --git a/tests/kittest/startup.rs b/tests/kittest/startup.rs deleted file mode 100644 index c5d93701..00000000 --- a/tests/kittest/startup.rs +++ /dev/null @@ -1,17 +0,0 @@ -use egui_kittest::Harness; - -/// Test that demonstrates basic app startup and shutdown with kittest -#[test] -fn test_app_startup() { - // Create a test harness for the egui app - // - let mut harness = Harness::builder().with_max_steps(100).build_eframe(|ctx| { - dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone()).with_animations(false) - }); - - // Set the window size - harness.set_size(egui::vec2(800.0, 600.0)); - - // Run one frame to ensure the app initializes - harness.run(); -}