selection-capture 0.1.3

Sync, cancellable selected-text capture engine with strategy-aware fallbacks
Documentation
use super::*;
use crate::windows_observer::windows_observer_test_lock;
use crate::windows_subscriber::windows_native_subscriber_stats;
use crate::WindowsObserverBridge;

#[derive(Debug)]
struct StubBackend {
    ui_automation: PlatformAttemptResult,
    iaccessible: PlatformAttemptResult,
    clipboard: PlatformAttemptResult,
    synthetic_copy: PlatformAttemptResult,
}

impl WindowsBackend for StubBackend {
    fn attempt_ui_automation(&self) -> PlatformAttemptResult {
        self.ui_automation.clone()
    }

    fn attempt_iaccessible(&self) -> PlatformAttemptResult {
        self.iaccessible.clone()
    }

    fn attempt_clipboard(&self) -> PlatformAttemptResult {
        self.clipboard.clone()
    }

    fn attempt_synthetic_copy(&self) -> PlatformAttemptResult {
        self.synthetic_copy.clone()
    }
}

#[test]
fn constructor_builds_stub_platform() {
    let platform = WindowsPlatform::new();
    let _ = platform;
}

#[test]
fn selection_monitor_default_poll_interval_is_stable() {
    let monitor = WindowsSelectionMonitor::default();
    assert_eq!(monitor.poll_interval, Duration::from_millis(120));
    assert_eq!(monitor.backend(), WindowsMonitorBackend::Polling);
}

#[test]
fn selection_monitor_emits_only_new_values() {
    let monitor = WindowsSelectionMonitor::new(Duration::from_millis(10));
    assert_eq!(
        monitor.emit_if_new("first".to_string()),
        Some("first".to_string())
    );
    assert_eq!(monitor.emit_if_new("first".to_string()), None);
    assert_eq!(
        monitor.emit_if_new("second".to_string()),
        Some("second".to_string())
    );
}

#[test]
fn selection_monitor_native_preferred_uses_event_pump_when_available() {
    let _guard = windows_observer_test_lock()
        .lock()
        .expect("test lock poisoned");
    fn pump() -> Vec<String> {
        vec![
            "  native a ".to_string(),
            "native a".to_string(),
            "native b".to_string(),
        ]
    }

    let monitor = WindowsSelectionMonitor::new_with_options(WindowsSelectionMonitorOptions {
        poll_interval: Duration::from_millis(10),
        backend: WindowsMonitorBackend::NativeEventPreferred,
        native_queue_capacity: 8,
        native_event_pump: Some(pump),
    });

    assert_eq!(
        monitor.next_selection_change(),
        Some("native a".to_string())
    );
    assert_eq!(
        monitor.next_selection_change(),
        Some("native b".to_string())
    );
}

#[test]
fn selection_monitor_native_preferred_uses_bridge_drain_by_default() {
    let _guard = windows_observer_test_lock()
        .lock()
        .expect("test lock poisoned");
    let _ = WindowsObserverBridge::stop();
    let _ = WindowsObserverBridge::start();
    assert!(WindowsObserverBridge::push_event("bridge one"));
    assert!(WindowsObserverBridge::push_event("bridge two"));

    let monitor = WindowsSelectionMonitor::new_with_options(WindowsSelectionMonitorOptions {
        poll_interval: Duration::from_millis(10),
        backend: WindowsMonitorBackend::NativeEventPreferred,
        native_queue_capacity: 8,
        native_event_pump: None,
    });

    assert_eq!(
        monitor.next_selection_change(),
        Some("bridge one".to_string())
    );
    assert_eq!(
        monitor.next_selection_change(),
        Some("bridge two".to_string())
    );
    assert!(WindowsObserverBridge::is_active());
    let _ = WindowsObserverBridge::stop();
}

#[test]
fn selection_monitor_native_preferred_releases_bridge_on_drop() {
    let _guard = windows_observer_test_lock()
        .lock()
        .expect("test lock poisoned");
    let _ = WindowsObserverBridge::stop();

    {
        let _monitor = WindowsSelectionMonitor::new_with_options(WindowsSelectionMonitorOptions {
            poll_interval: Duration::from_millis(10),
            backend: WindowsMonitorBackend::NativeEventPreferred,
            native_queue_capacity: 8,
            native_event_pump: None,
        });
        assert!(WindowsObserverBridge::is_active());
    }

    assert!(!WindowsObserverBridge::is_active());
}

#[test]
fn selection_monitor_native_preferred_transitions_subscriber_manager_lifecycle() {
    let _guard = windows_observer_test_lock()
        .lock()
        .expect("test lock poisoned");
    let _ = WindowsObserverBridge::stop();
    let before = windows_native_subscriber_stats();

    {
        let _monitor = WindowsSelectionMonitor::new_with_options(WindowsSelectionMonitorOptions {
            poll_interval: Duration::from_millis(10),
            backend: WindowsMonitorBackend::NativeEventPreferred,
            native_queue_capacity: 8,
            native_event_pump: None,
        });
        let during = windows_native_subscriber_stats();
        assert!(during.active);
        assert_eq!(during.starts, before.starts + 1);
    }

    let after = windows_native_subscriber_stats();
    assert!(after.stops > before.stops);
}

#[test]
fn selection_monitor_native_preferred_applies_queue_capacity() {
    let _guard = windows_observer_test_lock()
        .lock()
        .expect("test lock poisoned");
    let monitor = WindowsSelectionMonitor::new_with_options(WindowsSelectionMonitorOptions {
        poll_interval: Duration::from_millis(10),
        backend: WindowsMonitorBackend::NativeEventPreferred,
        native_queue_capacity: 2,
        native_event_pump: None,
    });
    let accepted = monitor.enqueue_native_selection_events(vec![
        "first".to_string(),
        "second".to_string(),
        "third".to_string(),
    ]);
    assert_eq!(accepted, 3);
    assert_eq!(monitor.native_queue_depth(), 2);
    assert_eq!(monitor.native_events_dropped(), 1);
    assert_eq!(monitor.next_selection_change(), Some("second".to_string()));
    assert_eq!(monitor.next_selection_change(), Some("third".to_string()));
}

#[test]
fn active_app_probe_does_not_panic() {
    let platform = WindowsPlatform::new();
    let _ = platform.active_app();
}

#[test]
fn dispatches_primary_accessibility_to_ui_automation() {
    let backend = StubBackend {
        ui_automation: PlatformAttemptResult::PermissionDenied,
        iaccessible: PlatformAttemptResult::Unavailable,
        clipboard: PlatformAttemptResult::Unavailable,
        synthetic_copy: PlatformAttemptResult::Unavailable,
    };

    let result = WindowsPlatform::dispatch_attempt(&backend, CaptureMethod::AccessibilityPrimary);

    assert_eq!(result, PlatformAttemptResult::PermissionDenied);
}

#[test]
fn dispatches_clipboard_methods_to_clipboard_attempt() {
    let backend = StubBackend {
        ui_automation: PlatformAttemptResult::Unavailable,
        iaccessible: PlatformAttemptResult::Unavailable,
        clipboard: PlatformAttemptResult::Success("clipboard".into()),
        synthetic_copy: PlatformAttemptResult::Success("synthetic".into()),
    };

    assert_eq!(
        WindowsPlatform::dispatch_attempt(&backend, CaptureMethod::ClipboardBorrow),
        PlatformAttemptResult::Success("clipboard".into())
    );
}

#[test]
fn dispatches_synthetic_copy_to_synthetic_copy_attempt() {
    let backend = StubBackend {
        ui_automation: PlatformAttemptResult::Unavailable,
        iaccessible: PlatformAttemptResult::Unavailable,
        clipboard: PlatformAttemptResult::Success("clipboard".into()),
        synthetic_copy: PlatformAttemptResult::Success("synthetic".into()),
    };

    assert_eq!(
        WindowsPlatform::dispatch_attempt(&backend, CaptureMethod::SyntheticCopy),
        PlatformAttemptResult::Success("synthetic".into())
    );
}

#[cfg(target_os = "windows")]
#[test]
fn normalizes_windows_text_stdout_and_strips_trailing_newline() {
    let raw = "line one\r\nline two\r\n";
    assert_eq!(
        normalize_windows_text_stdout(raw),
        Some("line one\nline two".to_string())
    );
}

#[cfg(target_os = "windows")]
#[test]
fn returns_none_when_windows_text_stdout_is_effectively_empty() {
    assert_eq!(normalize_windows_text_stdout("\r\n"), None);
    assert_eq!(normalize_windows_text_stdout(""), None);
}

#[cfg(target_os = "windows")]
#[test]
fn parses_active_app_stdout_with_path() {
    let raw = "NAME:Code\nPATH:C:\\Program Files\\Microsoft VS Code\\Code.exe\n";
    let parsed = parse_active_app_stdout(raw).expect("active app");

    assert_eq!(parsed.name, "Code");
    assert_eq!(
        parsed.bundle_id,
        "C:\\Program Files\\Microsoft VS Code\\Code.exe"
    );
}

#[cfg(target_os = "windows")]
#[test]
fn parses_active_app_stdout_without_path_uses_process_fallback() {
    let raw = "NAME:Notepad\nPATH:\n";
    let parsed = parse_active_app_stdout(raw).expect("active app");

    assert_eq!(parsed.name, "Notepad");
    assert_eq!(parsed.bundle_id, "process://notepad");
}