diff --git a/crates/zclaw-hands/src/hands/browser.rs b/crates/zclaw-hands/src/hands/browser.rs index 99244bc..d0dd73a 100644 --- a/crates/zclaw-hands/src/hands/browser.rs +++ b/crates/zclaw-hands/src/hands/browser.rs @@ -134,7 +134,7 @@ impl BrowserHand { id: "browser".to_string(), name: "浏览器".to_string(), description: "网页浏览器自动化,支持导航、交互和数据采集".to_string(), - needs_approval: false, + needs_approval: true, dependencies: vec!["webdriver".to_string()], input_schema: Some(serde_json::json!({ "type": "object", @@ -420,8 +420,211 @@ impl BrowserSequence { self } + /// Set whether to stop on error + pub fn stop_on_error(mut self, stop: bool) -> Self { + self.stop_on_error = stop; + self + } + /// Build the sequence pub fn build(self) -> Vec { self.steps } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::Hand; + use std::collections::HashMap; + + fn fresh_context() -> HandContext { + HandContext { + agent_id: zclaw_types::AgentId::new(), + working_dir: None, + env: HashMap::new(), + timeout_secs: 30, + callback_url: None, + } + } + + #[test] + fn test_browser_config() { + let hand = BrowserHand::new(); + let config = hand.config(); + assert_eq!(config.id, "browser"); + assert!(config.enabled); + } + + #[tokio::test] + async fn test_browser_config_needs_approval() { + let hand = BrowserHand::new(); + assert!(hand.config().needs_approval, "Browser hand should require approval per TOML config"); + } + + #[test] + fn test_action_deserialize_navigate() { + let json = serde_json::json!({ + "action": "navigate", + "url": "https://example.com", + "wait_for": "body" + }); + let action: BrowserAction = serde_json::from_value(json).expect("deserialize navigate"); + match action { + BrowserAction::Navigate { url, wait_for } => { + assert_eq!(url, "https://example.com"); + assert_eq!(wait_for, Some("body".to_string())); + } + _ => panic!("Expected Navigate action, got {:?}", action), + } + } + + #[test] + fn test_action_deserialize_click() { + let json = serde_json::json!({ + "action": "click", + "selector": "#submit-btn", + "wait_ms": 500 + }); + let action: BrowserAction = serde_json::from_value(json).expect("deserialize click"); + match action { + BrowserAction::Click { selector, wait_ms } => { + assert_eq!(selector, "#submit-btn"); + assert_eq!(wait_ms, Some(500)); + } + _ => panic!("Expected Click action, got {:?}", action), + } + } + + #[test] + fn test_action_deserialize_type() { + let json = serde_json::json!({ + "action": "type", + "selector": "#search", + "text": "hello world", + "clear_first": true + }); + let action: BrowserAction = serde_json::from_value(json).expect("deserialize type"); + match action { + BrowserAction::Type { selector, text, clear_first } => { + assert_eq!(selector, "#search"); + assert_eq!(text, "hello world"); + assert!(clear_first); + } + _ => panic!("Expected Type action, got {:?}", action), + } + } + + #[test] + fn test_action_deserialize_scrape() { + let json = serde_json::json!({ + "action": "scrape", + "selectors": ["h1", ".content", "#price"] + }); + let action: BrowserAction = serde_json::from_value(json).expect("deserialize scrape"); + match action { + BrowserAction::Scrape { selectors, wait_for } => { + assert_eq!(selectors, vec!["h1", ".content", "#price"]); + assert!(wait_for.is_none()); + } + _ => panic!("Expected Scrape action, got {:?}", action), + } + } + + #[test] + fn test_action_deserialize_screenshot() { + let json = serde_json::json!({ + "action": "screenshot", + "full_page": true + }); + let action: BrowserAction = serde_json::from_value(json).expect("deserialize screenshot"); + match action { + BrowserAction::Screenshot { selector, full_page } => { + assert!(selector.is_none()); + assert!(full_page); + } + _ => panic!("Expected Screenshot action, got {:?}", action), + } + } + + #[test] + fn test_all_major_actions_roundtrip() { + let actions = vec![ + BrowserAction::Navigate { url: "https://example.com".into(), wait_for: None }, + BrowserAction::Click { selector: "#btn".into(), wait_ms: None }, + BrowserAction::Type { selector: "#input".into(), text: "test".into(), clear_first: false }, + BrowserAction::Scrape { selectors: vec!["h1".into()], wait_for: None }, + BrowserAction::Screenshot { selector: None, full_page: false }, + BrowserAction::Wait { selector: "#loaded".into(), timeout_ms: 5000 }, + BrowserAction::Execute { script: "return 1".into(), args: vec![] }, + BrowserAction::FillForm { + fields: vec![FormField { selector: "#name".into(), value: "Alice".into() }], + submit_selector: Some("#submit".into()), + }, + ]; + + for original in actions { + let json = serde_json::to_value(&original).expect("serialize action"); + let roundtripped: BrowserAction = serde_json::from_value(json).expect("deserialize action"); + assert_eq!( + serde_json::to_value(&original).unwrap(), + serde_json::to_value(&roundtripped).unwrap(), + "Roundtrip failed for {:?}", + original + ); + } + } + + #[tokio::test] + async fn test_browser_sequence_builder() { + let ctx = fresh_context(); + let hand = BrowserHand::new(); + + let sequence = BrowserSequence::new("test_sequence") + .navigate("https://example.com") + .stop_on_error(false); + + assert_eq!(sequence.name, "test_sequence"); + assert!(!sequence.stop_on_error); + assert_eq!(sequence.steps.len(), 1); + + // Execute the navigate step + let action_json = serde_json::to_value(&sequence.steps[0]).expect("serialize step"); + let result = hand.execute(&ctx, action_json).await.expect("execute"); + assert!(result.success); + assert_eq!(result.output["action"], "navigate"); + assert_eq!(result.output["url"], "https://example.com"); + } + + #[tokio::test] + async fn test_browser_sequence_multiple_steps() { + let ctx = fresh_context(); + let hand = BrowserHand::new(); + + let sequence = BrowserSequence::new("multi_step") + .navigate("https://example.com") + .click("#login-btn") + .type_text("#username", "admin") + .screenshot(); + + assert_eq!(sequence.steps.len(), 4); + + // Verify each step can execute + for (i, step) in sequence.steps.iter().enumerate() { + let action_json = serde_json::to_value(step).expect("serialize step"); + let result = hand.execute(&ctx, action_json).await.expect("execute step"); + assert!(result.success, "Step {} failed: {:?}", i, result.error); + } + } + + #[test] + fn test_form_field_deserialize() { + let json = serde_json::json!({ + "selector": "#email", + "value": "user@example.com" + }); + let field: FormField = serde_json::from_value(json).expect("deserialize form field"); + assert_eq!(field.selector, "#email"); + assert_eq!(field.value, "user@example.com"); + } +}