//! Types representing accessibility tree snapshots from camofox-browser. use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// A page snapshot returned by the camofox-browser API. /// /// The snapshot is an accessibility tree with stable element refs /// (e.g., `e1`, `e2`) that can be used for clicking and typing. /// This format is ~90% more token-efficient than raw HTML. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageSnapshot { /// Accessibility tree text with element refs. pub snapshot: String, /// Optional URL of the page. #[serde(default)] pub url: String, /// Optional page title. #[serde(default)] pub title: String, /// Optional screenshot as base64 PNG. #[serde(default, skip_serializing_if = "Option::is_none")] pub screenshot: Option, /// Element reference map: ref -> element info. #[serde(default)] pub elements: HashMap, } /// Information about a single element in the snapshot. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ElementInfo { /// The element's tag name (e.g., "a", "button", "input"). #[serde(default)] pub tag: String, /// Visible text content. #[serde(default)] pub text: String, /// URL for links. #[serde(default, skip_serializing_if = "Option::is_none")] pub href: Option, /// Whether the element is interactive. #[serde(default)] pub interactive: bool, } impl PageSnapshot { /// Create a new snapshot from raw text. pub fn new(snapshot: String) -> Self { Self { snapshot, url: String::new(), title: String::new(), screenshot: None, elements: HashMap::new(), } } /// Whether the snapshot is empty (no content). pub fn is_empty(&self) -> bool { self.snapshot.is_empty() } /// Number of characters in the snapshot text. pub fn len(&self) -> usize { self.snapshot.len() } /// Extract all element refs from the snapshot text. /// /// Refs look like `[e1]`, `[e2]` in the accessibility tree. pub fn extract_refs(&self) -> Vec { let mut refs = Vec::new(); for line in self.snapshot.lines() { // Pattern: [eN] or eN if let Some(start) = line.find("[e") { let end = line[start + 2..].find(']').map(|i| start + 2 + i); if let Some(end_pos) = end { let ref_str = &line[start + 1..start + 1 + end_pos + 1]; refs.push(ref_str.to_string()); } } } refs.sort(); refs.dedup(); refs } } /// Links extracted from a page. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageLinks { /// List of links on the page. pub links: Vec, /// Total count of links. pub total: usize, } /// A single link item. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LinkItem { /// Link text. pub text: String, /// Target URL. pub href: String, /// Optional title attribute. #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, } /// Response from page navigation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NavigationResult { /// Final URL after navigation. pub url: String, /// Page title. pub title: String, /// Whether navigation succeeded. pub ok: bool, } /// Response from element interaction (click/type). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InteractionResult { /// Whether the interaction succeeded. pub ok: bool, /// New snapshot after interaction (if available). #[serde(default, skip_serializing_if = "Option::is_none")] pub snapshot: Option, } /// Health check response from camofox-browser. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealthResponse { /// Server is OK. pub ok: bool, /// Engine type (e.g., "camoufox"). pub engine: String, /// Whether browser is connected. #[serde(default)] pub browser_connected: bool, /// Whether browser is running. #[serde(default)] pub browser_running: bool, /// Number of active tabs. #[serde(default)] pub active_tabs: u32, /// Number of active sessions. #[serde(default)] pub active_sessions: u32, } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_refs_from_snapshot() { let snap = PageSnapshot::new( "- heading \"Example\" [level=1]\n- link \"Click\" [e1]:\n - /url: https://example.com\n- paragraph: text [e2]\n".into(), ); let refs = snap.extract_refs(); assert!(refs.contains(&"[e1]".to_string())); assert!(refs.contains(&"[e2]".to_string())); } #[test] fn test_empty_snapshot() { let snap = PageSnapshot::new(String::new()); assert!(snap.is_empty()); assert_eq!(snap.len(), 0); } #[test] fn test_health_response_deserialize() { let json = r#"{"ok":true,"engine":"camoufox","browserConnected":true,"browserRunning":true,"activeTabs":3,"activeSessions":2}"#; let health: HealthResponse = serde_json::from_str(json).unwrap(); assert!(health.ok); assert_eq!(health.active_tabs, 3); } }