browsing 0.1.7

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation
//! Additional tests for tools service

use browsing::actor::Page;
use browsing::browser::cdp::CdpClient;
use browsing::browser::views::{SessionInfo, TabInfo};
use browsing::observability::{RecorderConfig, SessionRecorder};
use browsing::tools::service::Tools;
use browsing::tools::views::{ActionModel, RegisteredAction};
use browsing::traits::BrowserClient;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;

// Minimal mock BrowserClient for testing Tools::act
struct MockBrowserClient {
    current_url: String,
}

impl MockBrowserClient {
    fn new() -> Self {
        Self {
            current_url: "about:blank".to_string(),
        }
    }
}

#[async_trait::async_trait]
impl BrowserClient for MockBrowserClient {
    async fn start(&mut self) -> browsing::error::Result<()> {
        Ok(())
    }

    async fn navigate(&mut self, url: &str) -> browsing::error::Result<()> {
        self.current_url = url.to_string();
        Ok(())
    }

    async fn get_current_url(&self) -> browsing::error::Result<String> {
        Ok(self.current_url.clone())
    }

    async fn create_tab(&mut self, _url: Option<&str>) -> browsing::error::Result<String> {
        Ok("mock-tab".to_string())
    }

    async fn switch_to_tab(&mut self, _target_id: &str) -> browsing::error::Result<()> {
        Ok(())
    }

    async fn close_tab(&mut self, _target_id: &str) -> browsing::error::Result<()> {
        Ok(())
    }

    async fn get_tabs(&self) -> browsing::error::Result<Vec<TabInfo>> {
        Ok(vec![])
    }

    async fn get_target_id_from_tab_id(&self, _tab_id: &str) -> browsing::error::Result<String> {
        Ok("mock-target".to_string())
    }

    fn get_page(&self) -> browsing::error::Result<Page> {
        Err(browsing::error::BrowsingError::Browser(
            "Mock browser".to_string(),
        ))
    }

    async fn take_screenshot(
        &self,
        _path: Option<&str>,
        _full_page: bool,
    ) -> browsing::error::Result<Vec<u8>> {
        Ok(vec![])
    }

    async fn get_current_page_title(&self) -> browsing::error::Result<String> {
        Ok("Mock".to_string())
    }

    fn get_cdp_client(&self) -> browsing::error::Result<Arc<CdpClient>> {
        Err(browsing::error::BrowsingError::Browser(
            "Mock browser".to_string(),
        ))
    }

    fn get_session_id(&self) -> browsing::error::Result<String> {
        Ok("mock-session".to_string())
    }

    fn get_current_target_id(&self) -> browsing::error::Result<String> {
        Ok("mock-target".to_string())
    }

    async fn get_session_info(&self) -> browsing::error::Result<SessionInfo> {
        Ok(SessionInfo {
            url: self.current_url.clone(),
            title: "Mock".to_string(),
            target_id: "mock-target".to_string(),
            session_id: "mock-session".to_string(),
        })
    }
}

#[test]
fn test_tools_registry_actions() {
    let tools = Tools::new(vec![]);

    // Check that default actions are registered
    assert!(tools.registry.registry.actions.contains_key("search"));
    assert!(tools.registry.registry.actions.contains_key("navigate"));
    assert!(tools.registry.registry.actions.contains_key("click"));
    assert!(tools.registry.registry.actions.contains_key("done"));
}

#[test]
fn test_tools_exclude_actions() {
    let tools = Tools::new(vec!["search".to_string(), "click".to_string()]);

    // Actions should still be registered (exclusion is handled at execution time)
    assert!(tools.registry.registry.actions.contains_key("navigate"));
}

#[test]
fn test_registered_action_structure() {
    let action = RegisteredAction {
        name: "test_action".to_string(),
        description: "Test action".to_string(),
        domains: None,
        handler: None,
    };

    assert_eq!(action.name, "test_action");
    assert_eq!(action.description, "Test action");
    assert!(action.domains.is_none());
}

#[test]
fn test_action_model_get_index() {
    let mut params = HashMap::new();
    params.insert("index".to_string(), json!(5));

    let action = ActionModel {
        action_type: "click".to_string(),
        params,
    };

    // get_index looks in nested objects, so this might not work directly
    // But we can test the structure
    assert_eq!(action.action_type, "click");
    assert!(action.params.contains_key("index"));
}

#[test]
fn test_action_model_set_index() {
    let mut params = HashMap::new();
    params.insert("index".to_string(), json!(1));

    let mut action = ActionModel {
        action_type: "click".to_string(),
        params,
    };

    // set_index modifies nested objects, but we can test the structure
    action.set_index(10);
    assert_eq!(action.action_type, "click");
}

#[tokio::test]
async fn test_tools_act_records_dom_snapshot() {
    let tools = Tools::new(vec![]);
    let mut browser = MockBrowserClient::new();
    let mut recorder = SessionRecorder::new(RecorderConfig {
        capture_dom: true,
        max_dom_chars: 1000,
        max_steps: 100,
    });

    let mut params = HashMap::new();
    params.insert("url".to_string(), json!("https://example.com"));
    let action = ActionModel {
        action_type: "navigate".to_string(),
        params,
    };

    let dom = "<html><body>test</body></html>".to_string();
    let result = tools
        .act(action, &mut browser, None, None, Some(&mut recorder), Some(dom.clone()))
        .await;

    assert!(result.is_ok());
    assert_eq!(recorder.len(), 1);
    assert_eq!(recorder.steps()[0].dom_snapshot, Some(dom));
}

#[tokio::test]
async fn test_tools_act_omits_dom_when_disabled() {
    let tools = Tools::new(vec![]);
    let mut browser = MockBrowserClient::new();
    let mut recorder = SessionRecorder::new(RecorderConfig {
        capture_dom: false,
        max_dom_chars: 0,
        max_steps: 100,
    });

    let mut params = HashMap::new();
    params.insert("url".to_string(), json!("https://example.com"));
    let action = ActionModel {
        action_type: "navigate".to_string(),
        params,
    };

    let result = tools
        .act(
            action,
            &mut browser,
            None,
            None,
            Some(&mut recorder),
            Some("<html></html>".to_string()),
        )
        .await;

    assert!(result.is_ok());
    assert_eq!(recorder.len(), 1);
    assert!(recorder.steps()[0].dom_snapshot.is_none());
}