diff --git a/crates/coldvox-text-injection/build.rs b/crates/coldvox-text-injection/build.rs index 1fa1a509..87eec3a1 100644 --- a/crates/coldvox-text-injection/build.rs +++ b/crates/coldvox-text-injection/build.rs @@ -3,94 +3,64 @@ use std::path::Path; use std::process::Command; fn main() { - // We only need to build the test applications if the `real-injection-tests` feature is enabled. - // This avoids adding build-time dependencies for regular users. - if env::var("CARGO_FEATURE_REAL_INJECTION_TESTS").is_ok() { - build_gtk_test_app(); - build_terminal_test_app(); - } -} - -fn build_gtk_test_app() { - println!("cargo:rerun-if-changed=test-apps/gtk_test_app.c"); - - let out_dir = env::var("OUT_DIR").unwrap(); - let executable_path = Path::new(&out_dir).join("gtk_test_app"); - - // Check if pkg-config and GTK3 are available before attempting to build. - let check_pkg_config = Command::new("pkg-config") - .arg("--atleast-version=3.0") - .arg("gtk+-3.0") - .status(); - - if check_pkg_config.is_err() || !check_pkg_config.unwrap().success() { - println!("cargo:warning=Skipping GTK test app build: GTK+ 3.0 not found by pkg-config."); + // Only run this build script if the `real-injection-tests` feature is enabled. + if env::var("CARGO_CFG_FEATURE_REAL_INJECTION_TESTS").is_err() { + println!("cargo:warning=Skipping build of test apps, real-injection-tests feature is not enabled."); return; } - // Get compiler flags from pkg-config. - let cflags_output = Command::new("pkg-config") - .arg("--cflags") - .arg("gtk+-3.0") - .output() - .expect("Failed to run pkg-config for cflags"); + let out_dir = env::var("OUT_DIR").expect("OUT_DIR environment variable not set."); + let src_path = Path::new("test-apps").join("text-capture-app.c"); - // Get linker flags from pkg-config. - let libs_output = Command::new("pkg-config") - .arg("--libs") - .arg("gtk+-3.0") - .output() - .expect("Failed to run pkg-config for libs"); + // --- Compile GTK Test App --- + let gtk_app_out = Path::new(&out_dir).join("gtk_test_app"); - let cflags = String::from_utf8(cflags_output.stdout).unwrap(); - let libs = String::from_utf8(libs_output.stdout).unwrap(); + // Use pkg-config to get GTK flags. + let gtk_cflags = pkg_config::Config::new().probe("gtk+-3.0").unwrap(); + let mut command = Command::new("cc"); + command.arg("-o").arg(>k_app_out); + for flag in >k_cflags.cflags { + command.arg(flag); + } + command.arg(&src_path); + for lib_path in >k_cflags.link_paths { + command.arg(format!("-L{}", lib_path.to_string_lossy())); + } + for lib in >k_cflags.libs { + command.arg(format!("-l{}", lib)); + } - // Compile the test app using gcc. - let status = Command::new("gcc") - .arg("test-apps/gtk_test_app.c") - .arg("-o") - .arg(&executable_path) - .args(cflags.split_whitespace()) - .args(libs.split_whitespace()) + let status = command .status() - .expect("Failed to execute gcc"); + .expect("Failed to compile GTK test app with cc. Is a C compiler and GTK dev libraries installed?"); if !status.success() { - println!("cargo:warning=Failed to compile GTK test app. Real injection tests against GTK may fail."); + panic!("Failed to compile the GTK test app."); } -} - -fn build_terminal_test_app() { - println!("cargo:rerun-if-changed=test-apps/terminal-test-app/src/main.rs"); - println!("cargo:rerun-if-changed=test-apps/terminal-test-app/Cargo.toml"); - let out_dir = env::var("OUT_DIR").unwrap(); - let target_dir = Path::new(&out_dir).join("terminal-test-app-target"); + println!( + "cargo:warning=Successfully compiled GTK test app to: {:?}", + gtk_app_out + ); - // Build the terminal test app using `cargo build`. - let status = Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".to_string())) - .arg("build") - .arg("--package") - .arg("terminal-test-app") - .arg("--release") // Build in release mode for faster startup. - .arg("--target-dir") - .arg(&target_dir) + // --- Compile Terminal Test App --- + let term_app_out = Path::new(&out_dir).join("terminal_test_app"); + let status = Command::new("cc") + .arg("-o") + .arg(&term_app_out) + .arg("-DTERMINAL_MODE") + .arg(&src_path) .status() - .expect("Failed to execute cargo build for terminal-test-app"); + .expect("Failed to compile terminal test app with cc. Is a C compiler installed?"); if !status.success() { - println!( - "cargo:warning=Failed to build the terminal test app. Real injection tests may fail." - ); - } else { - // Copy the executable to a known location in OUT_DIR for the tests to find easily. - let src_path = target_dir.join("release/terminal-test-app"); - let dest_path = Path::new(&out_dir).join("terminal-test-app"); - if let Err(e) = std::fs::copy(&src_path, &dest_path) { - println!( - "cargo:warning=Failed to copy terminal test app executable from {:?} to {:?}: {}", - src_path, dest_path, e - ); - } + panic!("Failed to compile the terminal test app."); } + + println!( + "cargo:warning=Successfully compiled terminal test app to: {:?}", + term_app_out + ); + + println!("cargo:rerun-if-changed=test-apps/text-capture-app.c"); } diff --git a/crates/coldvox-text-injection/src/tests/mod.rs b/crates/coldvox-text-injection/src/tests/mod.rs index b576f697..b46902a2 100644 --- a/crates/coldvox-text-injection/src/tests/mod.rs +++ b/crates/coldvox-text-injection/src/tests/mod.rs @@ -1,7 +1,17 @@ -//! Test modules for coldvox-text-injection +// The `real-injection-tests` feature enables tests that interact with a live desktop environment. +// These tests are disabled by default as they require a graphical session (X11 or Wayland). +// To run these tests: `cargo test -p coldvox-text-injection --features real-injection-tests` +#[cfg(feature = "real-injection-tests")] +pub mod real_injection; -// pub mod real_injection; +// Shared test utilities for both unit and real injection tests. +pub mod test_harness; pub mod test_utils; -pub mod wl_copy_basic_test; -pub mod wl_copy_simple_test; -pub mod wl_copy_stdin_test; + +// Unit tests for wl-clipboard-rs integration +#[cfg(feature = "wl_clipboard")] +mod wl_copy_basic_test; +#[cfg(feature = "wl_clipboard")] +mod wl_copy_simple_test; +#[cfg(feature = "wl_clipboard")] +mod wl_copy_stdin_test; diff --git a/crates/coldvox-text-injection/src/tests/real_injection.rs.disabled b/crates/coldvox-text-injection/src/tests/real_injection.rs.disabled deleted file mode 100644 index 82d5b5c1..00000000 --- a/crates/coldvox-text-injection/src/tests/real_injection.rs.disabled +++ /dev/null @@ -1,356 +0,0 @@ -//! # Real Injection Tests -//! -//! This module contains tests that perform real text injection into lightweight -//! test applications. These tests require a graphical environment (X11 or Wayland) -//! and are therefore ignored by default. -//! -//! To run these tests, use the following command: -//! `cargo test -p coldvox-text-injection --features real-injection-tests` - -// NOTE: Using modular injectors from the injectors module -#[cfg(feature = "wl_clipboard")] -use crate::clipboard_paste_injector::ClipboardPasteInjector; -#[cfg(feature = "enigo")] -use crate::enigo_injector::EnigoInjector; -#[cfg(feature = "atspi")] -use crate::injectors::atspi::AtspiInjector; -#[cfg(feature = "ydotool")] -use crate::ydotool_injector::YdotoolInjector; -// Bring trait into scope so async trait methods (inject_text, is_available) resolve. -use crate::TextInjector; - -use crate::tests::test_harness::{verify_injection, TestApp, TestAppManager, TestEnvironment}; -use std::time::Duration; - -/// A placeholder test to verify that the test harness, build script, and -/// environment detection are all working correctly. -#[tokio::test] - -async fn harness_self_test_launch_gtk_app() { - let env = TestEnvironment::current(); - if !env.can_run_real_tests() { - eprintln!("Skipping real injection test: no display server found."); - return; - } - - println!("Attempting to launch GTK test app..."); - let app_handle = TestAppManager::launch_gtk_app() - .expect("Failed to launch GTK test app. Check build.rs output and ensure GTK3 dev libraries are installed."); - - // The app should be running. We'll give it a moment to stabilize. - tokio::time::sleep(Duration::from_millis(200)).await; - - // The test passes if the app launches without error and is cleaned up. - // The cleanup is handled by the `Drop` implementation of `TestApp`. - println!( - "GTK test app launched successfully and will be cleaned up. PID: {}", - app_handle.pid - ); -} - -/// Waits for the test application to be ready by polling for its output file. -/// This is much faster than a fixed-duration sleep. -async fn wait_for_app_ready(app: &TestApp) { - let max_wait = Duration::from_secs(5); - let poll_interval = Duration::from_millis(50); - let start_time = std::time::Instant::now(); - - while start_time.elapsed() < max_wait { - if app.output_file.exists() { - // A small extra delay to ensure the app is fully interactive - tokio::time::sleep(Duration::from_millis(50)).await; - return; - } - tokio::time::sleep(poll_interval).await; - } - panic!("Test application did not become ready within 5 seconds."); -} - -//--- AT-SPI Tests --- - -/// Helper function to run a complete injection and verification test for the AT-SPI backend. -async fn run_atspi_test(test_text: &str) { - let env = TestEnvironment::current(); - if !env.can_run_real_tests() { - // This check is technically redundant if the tests are run with the top-level skip, - // but it's good practice to keep it for clarity and direct execution. - eprintln!("Skipping AT-SPI test: no display server found."); - return; - } - - let app = TestAppManager::launch_gtk_app().expect("Failed to launch GTK app."); - - // Allow time for the app to initialize and for the AT-SPI bus to register it. - // This is a common requirement in UI testing. - tokio::time::sleep(Duration::from_millis(500)).await; - // Wait for the app to be fully initialized before interacting with it. - wait_for_app_ready(&app).await; - - #[cfg(feature = "atspi")] - { - let injector = AtspiInjector::new(Default::default()); - if !injector.is_available().await { - println!( - "Skipping AT-SPI test: backend is not available (is at-spi-bus-launcher running?)." - ); - return; - } - - injector.inject_text(test_text).await.unwrap_or_else(|e| { - panic!("AT-SPI injection failed for text '{}': {:?}", test_text, e) - }); - } - - #[cfg(not(feature = "atspi"))] - { - println!("Skipping AT-SPI test: atspi feature not enabled"); - } - - verify_injection(&app.output_file, test_text) - .await - .unwrap_or_else(|e| { - panic!( - "Verification failed for AT-SPI with text '{}': {}", - test_text, e - ) - }); -} - -#[tokio::test] - -async fn test_atspi_simple_text() { - run_atspi_test("Hello from AT-SPI!").await; -} - -#[tokio::test] - -async fn test_atspi_unicode_text() { - run_atspi_test("Hello ColdVox 🎤 测试").await; -} - -#[tokio::test] - -async fn test_atspi_long_text() { - // A long string to test for buffer issues. - let long_text = - "This is a long string designed to test the injection capabilities of the backend. " - .repeat(50); - assert!(long_text.len() > 1000); - run_atspi_test(&long_text).await; -} - -#[tokio::test] - -async fn test_atspi_special_chars() { - run_atspi_test("Line 1\nLine 2\twith a tab\nAnd some symbols: !@#$%^&*()_+").await; -} - -//--- Ydotool Tests --- -#[cfg(feature = "ydotool")] - -/// Helper function to run a complete injection and verification test for the ydotool backend. -/// This test involves setting the clipboard, as ydotool's primary injection method is paste. -async fn run_ydotool_test(test_text: &str) { - let env = TestEnvironment::current(); - if !env.can_run_real_tests() { - eprintln!("Skipping ydotool test: no display server found."); - return; - } - - // ydotool requires a running daemon and access to /dev/uinput. - // The injector's `is_available` check will handle this. - let injector = YdotoolInjector::new(Default::default()); - if !injector.is_available().await { - println!("Skipping ydotool test: backend is not available (is ydotool daemon running?)."); - return; - } - - // Set the clipboard content. We use `arboard` as it works on both X11 and Wayland. - let mut clipboard = arboard::Clipboard::new().expect("Failed to create clipboard context."); - clipboard - .set_text(test_text.to_string()) - .expect("Failed to set clipboard text."); - - // Verify that clipboard content was set correctly before proceeding - let clipboard_content = clipboard.get_text().expect("Failed to get clipboard text."); - assert_eq!( - clipboard_content, test_text, - "Clipboard content was not set correctly." - ); - - let app = TestAppManager::launch_gtk_app().expect("Failed to launch GTK app."); - tokio::time::sleep(Duration::from_millis(500)).await; - wait_for_app_ready(&app).await; - - // The inject_text for ydotool will trigger a paste (Ctrl+V). - injector - .inject_text(test_text) - .await - .unwrap_or_else(|e| panic!("ydotool injection failed for text '{}': {:?}", test_text, e)); - - verify_injection(&app.output_file, test_text) - .await - .unwrap_or_else(|e| { - panic!( - "Verification failed for ydotool with text '{}': {}", - test_text, e - ) - }); -} - -#[tokio::test] -#[cfg(feature = "ydotool")] - -async fn test_ydotool_simple_text() { - run_ydotool_test("Hello from ydotool!").await; -} - -#[tokio::test] -#[cfg(feature = "ydotool")] - -async fn test_ydotool_unicode_text() { - run_ydotool_test("Hello ColdVox 🎤 测试 (via ydotool)").await; -} - -#[tokio::test] -#[cfg(feature = "ydotool")] - -async fn test_ydotool_long_text() { - let long_text = "This is a long string for ydotool. ".repeat(50); - assert!(long_text.len() > 1000); - run_ydotool_test(&long_text).await; -} - -#[tokio::test] -#[cfg(feature = "ydotool")] - -async fn test_ydotool_special_chars() { - run_ydotool_test("ydotool line 1\nydotool line 2\twith tab").await; -} - -//--- Clipboard + Paste Tests --- - -/// Helper to test clipboard injection followed by a paste action. -/// This simulates a realistic clipboard workflow. -async fn run_clipboard_paste_test(test_text: &str) { - let env = TestEnvironment::current(); - if !env.can_run_real_tests() { - eprintln!("Skipping clipboard test: no display server found."); - return; - } - - // This test requires both a clipboard manager and a paste mechanism. - // We use ClipboardInjector (Wayland) and Enigo (cross-platform paste). - #[cfg(all(feature = "wl_clipboard", feature = "enigo"))] - { - // Use ClipboardPasteInjector which sets clipboard and attempts a paste (via AT-SPI/ydotool). - let clipboard_paste = ClipboardPasteInjector::new(Default::default()); - if !clipboard_paste.is_available().await { - println!("Skipping clipboard test: backend is not available (not on Wayland?)."); - return; - } - - // Launch the app to paste into. - let app = TestAppManager::launch_gtk_app().expect("Failed to launch GTK app."); - tokio::time::sleep(Duration::from_millis(500)).await; - wait_for_app_ready(&app).await; - - // Perform clipboard+paste using the combined injector (it will try AT-SPI first then ydotool). - clipboard_paste - .inject_text(test_text) - .await - .expect("Clipboard+paste injection failed."); - - // Verify the result. - verify_injection(&app.output_file, test_text) - .await - .unwrap_or_else(|e| { - panic!( - "Verification failed for clipboard paste with text '{}': {}", - test_text, e - ) - }); - } - - #[cfg(not(all(feature = "wl_clipboard", feature = "enigo")))] - { - println!("Skipping clipboard test: required features (wl_clipboard, enigo) not enabled"); - } -} - -#[tokio::test] - -async fn test_clipboard_simple_text() { - run_clipboard_paste_test("Hello from the clipboard!").await; -} - -#[tokio::test] - -async fn test_clipboard_unicode_text() { - run_clipboard_paste_test("Clipboard 🎤 and paste 🎤").await; -} - -//--- Enigo (Typing) Tests --- - -/// Helper to test the direct typing capability of the Enigo backend. -async fn run_enigo_typing_test(test_text: &str) { - let env = TestEnvironment::current(); - if !env.can_run_real_tests() { - eprintln!("Skipping enigo typing test: no display server found."); - return; - } - - #[cfg(feature = "enigo")] - { - let injector = EnigoInjector::new(Default::default()); - if !injector.is_available().await { - println!("Skipping enigo typing test: backend is not available."); - return; - } - - let app = TestAppManager::launch_gtk_app().expect("Failed to launch GTK app."); - tokio::time::sleep(Duration::from_millis(500)).await; - wait_for_app_ready(&app).await; - - // Use the test-only helper to force typing instead of pasting. - injector - .type_text_directly(test_text) - .await - .unwrap_or_else(|e| panic!("Enigo typing failed for text '{}': {:?}", test_text, e)); - - verify_injection(&app.output_file, test_text) - .await - .unwrap_or_else(|e| { - panic!( - "Verification failed for enigo typing with text '{}': {}", - test_text, e - ) - }); - } - - #[cfg(not(feature = "enigo"))] - { - println!("Skipping enigo typing test: enigo feature not enabled"); - } -} - -#[tokio::test] - -async fn test_enigo_typing_simple_text() { - run_enigo_typing_test("Enigo types this text.").await; -} - -#[tokio::test] - -async fn test_enigo_typing_unicode_text() { - // Note: Enigo's unicode support can be platform-dependent. This test will verify it. - run_enigo_typing_test("Enigo 🎤 typing 🎤 unicode").await; -} - -#[tokio::test] - -async fn test_enigo_typing_special_chars() { - run_enigo_typing_test("Enigo types\nnew lines and\ttabs.").await; -} - -// TODO(#40): Add tests for kdotool, combo injectors etc. diff --git a/crates/coldvox-text-injection/src/tests/real_injection/atspi.rs b/crates/coldvox-text-injection/src/tests/real_injection/atspi.rs new file mode 100644 index 00000000..2347a705 --- /dev/null +++ b/crates/coldvox-text-injection/src/tests/real_injection/atspi.rs @@ -0,0 +1,36 @@ +use crate::injectors::atspi::AtspiInjector; +use crate::tests::real_injection::run_test; +use crate::TextInjector; +use std::time::Duration; + +/// Helper function to run a complete injection and verification test for the AT-SPI backend. +async fn run_atspi_test(test_text: &str) { + let injector = AtspiInjector::new(Default::default()); + if !injector.is_available().await { + println!("Skipping AT-SPI test: backend is not available (is at-spi-bus-launcher running?)."); + return; + } + run_test(test_text, &injector).await; +} + +#[tokio::test] +async fn test_atspi_simple_text() { + run_atspi_test("Hello from AT-SPI!").await; +} + +#[tokio::test] +async fn test_atspi_unicode_text() { + run_atspi_test("Hello ColdVox 🎤 测试").await; +} + +#[tokio::test] +async fn test_atspi_long_text() { + let long_text = "This is a long string designed to test the injection capabilities of the backend. ".repeat(50); + assert!(long_text.len() > 1000); + run_atspi_test(&long_text).await; +} + +#[tokio::test] +async fn test_atspi_special_chars() { + run_atspi_test("Line 1\nLine 2\twith a tab\nAnd some symbols: !@#$%^&*()_+").await; +} diff --git a/crates/coldvox-text-injection/src/tests/real_injection/clipboard.rs b/crates/coldvox-text-injection/src/tests/real_injection/clipboard.rs new file mode 100644 index 00000000..f656d282 --- /dev/null +++ b/crates/coldvox-text-injection/src/tests/real_injection/clipboard.rs @@ -0,0 +1,28 @@ +#![cfg(all(feature = "wl_clipboard", feature = "enigo"))] + +use crate::injectors::ClipboardPasteInjector; +use crate::TextInjector; + +/// Helper to test clipboard injection followed by a paste action. +/// This simulates a realistic clipboard workflow. +async fn run_clipboard_paste_test(test_text: &str) { + // This test requires both a clipboard manager and a paste mechanism. + // We use ClipboardInjector (Wayland) and Enigo (cross-platform paste). + let clipboard_paste = ClipboardPasteInjector::new(Default::default()); + if !clipboard_paste.is_available().await { + println!("Skipping clipboard test: backend is not available (not on Wayland?)."); + return; + } + + crate::tests::real_injection::run_test(test_text, &clipboard_paste).await; +} + +#[tokio::test] +async fn test_clipboard_simple_text() { + run_clipboard_paste_test("Hello from the clipboard!").await; +} + +#[tokio::test] +async fn test_clipboard_unicode_text() { + run_clipboard_paste_test("Clipboard 🎤 and paste 🎤").await; +} diff --git a/crates/coldvox-text-injection/src/tests/real_injection/enigo.rs b/crates/coldvox-text-injection/src/tests/real_injection/enigo.rs new file mode 100644 index 00000000..f9249a3d --- /dev/null +++ b/crates/coldvox-text-injection/src/tests/real_injection/enigo.rs @@ -0,0 +1,55 @@ +#![cfg(feature = "enigo")] + +use crate::enigo_injector::EnigoInjector; +use crate::tests::test_harness::{verify_injection, TestAppManager, TestEnvironment}; +use crate::TextInjector; +use std::time::Duration; + +/// Helper to test the direct typing capability of the Enigo backend. +async fn run_enigo_typing_test(test_text: &str) { + let env = TestEnvironment::current(); + if !env.can_run_real_tests() { + println!("Skipping enigo typing test: no display server found."); + return; + } + + let injector = EnigoInjector::new(Default::default()); + if !injector.is_available().await { + println!("Skipping enigo typing test: backend is not available."); + return; + } + + let app = TestAppManager::launch_gtk_app().expect("Failed to launch GTK app."); + tokio::time::sleep(Duration::from_millis(500)).await; + + // Use the test-only helper to force typing instead of pasting. + injector + .type_text_directly(test_text) + .await + .unwrap_or_else(|e| panic!("Enigo typing failed for text '{}': {:?}", test_text, e)); + + verify_injection(&app.output_file, test_text) + .await + .unwrap_or_else(|e| { + panic!( + "Verification failed for enigo typing with text '{}': {}", + test_text, e + ) + }); +} + +#[tokio::test] +async fn test_enigo_typing_simple_text() { + run_enigo_typing_test("Enigo types this text.").await; +} + +#[tokio::test] +async fn test_enigo_typing_unicode_text() { + // Note: Enigo's unicode support can be platform-dependent. This test will verify it. + run_enigo_typing_test("Enigo 🎤 typing 🎤 unicode").await; +} + +#[tokio::test] +async fn test_enigo_typing_special_chars() { + run_enigo_typing_test("Enigo types\nnew lines and\ttabs.").await; +} diff --git a/crates/coldvox-text-injection/src/tests/real_injection/kdotool.rs b/crates/coldvox-text-injection/src/tests/real_injection/kdotool.rs new file mode 100644 index 00000000..79278f9b --- /dev/null +++ b/crates/coldvox-text-injection/src/tests/real_injection/kdotool.rs @@ -0,0 +1,30 @@ +#![cfg(feature = "kdotool")] + +use crate::kdotool_injector::KdotoolInjector; +use crate::TextInjector; + +/// Helper function to run a complete injection and verification test for the kdotool backend. +async fn run_kdotool_test(test_text: &str) { + let injector = KdotoolInjector::new(Default::default()); + if !injector.is_available().await { + println!("Skipping kdotool test: backend is not available (is kdotool running?)."); + return; + } + + crate::tests::real_injection::run_test(test_text, &injector).await; +} + +#[tokio::test] +async fn test_kdotool_simple_text() { + run_kdotool_test("Hello from kdotool!").await; +} + +#[tokio::test] +async fn test_kdotool_unicode_text() { + run_kdotool_test("Hello ColdVox 🎤 kdotool").await; +} + +#[tokio::test] +async fn test_kdotool_special_chars() { + run_kdotool_test("kdotool line 1\nkdotool line 2\twith tab").await; +} diff --git a/crates/coldvox-text-injection/src/tests/real_injection/mod.rs b/crates/coldvox-text-injection/src/tests/real_injection/mod.rs new file mode 100644 index 00000000..8e3e78f1 --- /dev/null +++ b/crates/coldvox-text-injection/src/tests/real_injection/mod.rs @@ -0,0 +1,91 @@ +//! # Real Injection Tests +//! +//! This module contains tests that perform real text injection into lightweight +//! test applications. These tests require a graphical environment (X11 or Wayland) +//! and are therefore ignored by default. +//! +//! To run these tests, use the following command: +//! `cargo test -p coldvox-text-injection --features real-injection-tests` + +use crate::tests::test_harness::{verify_injection, TestAppManager, TestEnvironment}; +use crate::TextInjector; +use std::time::Duration; + +// --- Test Modules --- +#[cfg(feature = "atspi")] +mod atspi; +#[cfg(all(feature = "wl_clipboard", feature = "enigo"))] +mod clipboard; +#[cfg(feature = "enigo")] +mod enigo; +#[cfg(feature = "kdotool")] +mod kdotool; +#[cfg(feature = "ydotool")] +mod ydotool; + +/// A generic test runner for injection backends. +/// +/// This function handles the boilerplate of setting up the test environment, +/// launching the test app, running the injection, and verifying the result. +pub async fn run_test(test_text: &str, injector: &dyn TextInjector) { + let env = TestEnvironment::current(); + if !env.can_run_real_tests() { + eprintln!( + "Skipping real injection test for backend '{}': no display server found.", + injector.backend_name() + ); + return; + } + + let app = TestAppManager::launch_gtk_app().expect("Failed to launch GTK app."); + // Allow time for the app to initialize and for the AT-SPI bus to register it. + tokio::time::sleep(Duration::from_millis(500)).await; + + injector + .inject_text(test_text, None) + .await + .unwrap_or_else(|e| { + panic!( + "Injection failed for backend '{}' with text '{}': {:?}", + injector.backend_name(), + test_text, + e + ) + }); + + verify_injection(&app.output_file, test_text) + .await + .unwrap_or_else(|e| { + panic!( + "Verification failed for backend '{}' with text '{}': {}", + injector.backend_name(), + test_text, + e + ) + }); +} + +/// A placeholder test to verify that the test harness, build script, and +/// environment detection are all working correctly. +#[tokio::test] +async fn harness_self_test_launch_gtk_app() { + let env = TestEnvironment::current(); + if !env.can_run_real_tests() { + eprintln!("Skipping real injection test: no display server found."); + return; + } + + println!("Attempting to launch GTK test app..."); + let app_handle = TestAppManager::launch_gtk_app() + .expect("Failed to launch GTK test app. Check build.rs output and ensure GTK3 dev libraries are installed."); + + // The app should be running. We'll give it a moment to stabilize. + tokio::time::sleep(Duration::from_millis(200)).await; + + // The test passes if the app launches without error and is cleaned up. + // The cleanup is handled by the `Drop` implementation of `TestApp`. + println!( + "GTK test app launched successfully and will be cleaned up. PID: {}", + app_handle.pid + ); +} diff --git a/crates/coldvox-text-injection/src/tests/real_injection/ydotool.rs b/crates/coldvox-text-injection/src/tests/real_injection/ydotool.rs new file mode 100644 index 00000000..bb02409c --- /dev/null +++ b/crates/coldvox-text-injection/src/tests/real_injection/ydotool.rs @@ -0,0 +1,54 @@ +#![cfg(feature = "ydotool")] + +use crate::ydotool_injector::YdotoolInjector; +use crate::TextInjector; + +/// Helper function to run a complete injection and verification test for the ydotool backend. +/// This test involves setting the clipboard, as ydotool's primary injection method is paste. +async fn run_ydotool_test(test_text: &str) { + // ydotool requires a running daemon and access to /dev/uinput. + // The injector's `is_available` check will handle this. + let injector = YdotoolInjector::new(Default::default()); + if !injector.is_available().await { + println!("Skipping ydotool test: backend is not available (is ydotool daemon running?)."); + return; + } + + // Set the clipboard content. We use `arboard` as it works on both X11 and Wayland. + let mut clipboard = arboard::Clipboard::new().expect("Failed to create clipboard context."); + clipboard + .set_text(test_text.to_string()) + .expect("Failed to set clipboard text."); + + // Verify that clipboard content was set correctly before proceeding + let clipboard_content = clipboard.get_text().expect("Failed to get clipboard text."); + assert_eq!( + clipboard_content, test_text, + "Clipboard content was not set correctly." + ); + + // The inject_text for ydotool will trigger a paste (Ctrl+V). + crate::tests::real_injection::run_test(test_text, &injector).await; +} + +#[tokio::test] +async fn test_ydotool_simple_text() { + run_ydotool_test("Hello from ydotool!").await; +} + +#[tokio::test] +async fn test_ydotool_unicode_text() { + run_ydotool_test("Hello ColdVox 🎤 测试 (via ydotool)").await; +} + +#[tokio::test] +async fn test_ydotool_long_text() { + let long_text = "This is a long string for ydotool. ".repeat(50); + assert!(long_text.len() > 1000); + run_ydotool_test(&long_text).await; +} + +#[tokio::test] +async fn test_ydotool_special_chars() { + run_ydotool_test("ydotool line 1\nydotool line 2\twith tab").await; +} diff --git a/crates/coldvox-text-injection/src/tests/test_harness.rs b/crates/coldvox-text-injection/src/tests/test_harness.rs index 6f6204c8..88dc4f42 100644 --- a/crates/coldvox-text-injection/src/tests/test_harness.rs +++ b/crates/coldvox-text-injection/src/tests/test_harness.rs @@ -152,7 +152,7 @@ impl TestAppManager { /// The application is expected to have been compiled by the `build.rs` script. pub fn launch_terminal_app() -> Result { let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set, build script did not run?"); - let exe_path = Path::new(&out_dir).join("terminal-test-app"); + let exe_path = Path::new(&out_dir).join("terminal_test_app"); if !exe_path.exists() { return Err(std::io::Error::new( diff --git a/crates/coldvox-text-injection/test-apps/text-capture-app.c b/crates/coldvox-text-injection/test-apps/text-capture-app.c new file mode 100644 index 00000000..4432ce29 --- /dev/null +++ b/crates/coldvox-text-injection/test-apps/text-capture-app.c @@ -0,0 +1,87 @@ +#include +#include +#include +#include + +#ifdef TERMINAL_MODE +// This code block is compiled only when the TERMINAL_MODE preprocessor directive is defined. +// It provides a simple command-line interface for capturing text. +void run_terminal_mode() { + char buffer[4096]; // A buffer to hold the input text. + // Determine the output file path. + char output_file[256]; + snprintf(output_file, sizeof(output_file), "/tmp/coldvox_terminal_test_%d.txt", getpid()); + + // Open the output file for writing. + FILE *fp = fopen(output_file, "w"); + if (fp == NULL) { + perror("Failed to open output file"); + exit(1); + } + + // Read from standard input line by line and write to the file. + while (fgets(buffer, sizeof(buffer), stdin) != NULL) { + fprintf(fp, "%s", buffer); + fflush(fp); // Ensure the text is written immediately. + } + + fclose(fp); +} +#else +// This code block is compiled when TERMINAL_MODE is not defined. +// It provides a GTK-based graphical interface for capturing text. +#include +// This callback is triggered whenever the text in the GtkTextView changes. +static void on_text_changed(GtkTextBuffer *buffer, gpointer user_data) { + GtkTextIter start, end; + gtk_text_buffer_get_start_iter(buffer, &start); + gtk_text_buffer_get_end_iter(buffer, &end); + gchar *text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); + + const char *output_file = (const char *)user_data; + FILE *fp = fopen(output_file, "w"); + if (fp != NULL) { + fprintf(fp, "%s", text); + fclose(fp); + } else { + perror("Failed to open output file"); + } + + g_free(text); +} + +void run_gtk_mode(int argc, char *argv[]) { + // Determine the output file path. + char output_file[256]; + snprintf(output_file, sizeof(output_file), "/tmp/coldvox_gtk_test_%d.txt", getpid()); + + gtk_init(&argc, &argv); + + GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "ColdVox Test App"); + gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); + g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL); + + GtkWidget *textview = gtk_text_view_new(); + GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview)); + + GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL); + gtk_container_add(GTK_CONTAINER(scrolled_window), textview); + gtk_container_add(GTK_CONTAINER(window), scrolled_window); + + g_signal_connect(buffer, "changed", G_CALLBACK(on_text_changed), output_file); + + gtk_widget_show_all(window); + + gtk_main(); +} +#endif + +int main(int argc, char *argv[]) { +#ifdef TERMINAL_MODE + run_terminal_mode(); +#else + run_gtk_mode(argc, argv); +#endif + return 0; +}