car_browser/backend.rs
1//! Browser backend trait — the abstract interface that all browser implementations must satisfy.
2//!
3//! This maps 1:1 to human-equivalent perception and input (Manifesto Principles 3-5).
4
5use async_trait::async_trait;
6use thiserror::Error;
7
8use crate::models::{A11yNode, CookieParam, Modifier, Viewport, WaitCondition};
9
10/// Errors that can occur in browser operations.
11#[derive(Error, Debug)]
12pub enum BrowserError {
13 #[error("Screenshot capture failed: {0}")]
14 ScreenshotFailed(String),
15
16 #[error("Accessibility tree extraction failed: {0}")]
17 AccessibilityFailed(String),
18
19 #[error("Navigation failed: {0}")]
20 NavigationFailed(String),
21
22 #[error("Input injection failed: {0}")]
23 InputFailed(String),
24
25 #[error("Element not found: {0}")]
26 ElementNotFound(String),
27
28 #[error("Platform internal error: {0}")]
29 PlatformInternal(String),
30
31 #[error("Wait condition timed out")]
32 Timeout,
33
34 #[error("Browser not available: {0}")]
35 NotAvailable(String),
36
37 #[error("Not supported: {0}")]
38 Unsupported(String),
39}
40
41/// Abstract browser backend trait.
42///
43/// Implementations drive a real browser (Tauri WebView, headless Chromium, etc.)
44/// through human-equivalent perception and input only.
45///
46/// # Perception (what the AI can see)
47/// - Screenshots: rendered pixels
48/// - Accessibility tree: semantic structure exposed to assistive technologies
49///
50/// # Actions (what the AI can do)
51/// - Click, type, scroll, keypress — all map 1:1 to human input
52/// - Navigation — equivalent to typing a URL
53///
54/// # Disallowed
55/// - DOM traversal, JS execution for data extraction, hidden attributes
56/// - Network traffic inspection, cookie/storage introspection
57#[async_trait]
58pub trait BrowserBackend: Send + Sync {
59 // =========================================================================
60 // Perception
61 // =========================================================================
62
63 /// Capture a screenshot of the current page as PNG data.
64 async fn capture_screenshot(&self) -> Result<Vec<u8>, BrowserError>;
65
66 /// Extract the accessibility tree from the current page.
67 async fn get_accessibility_tree(&self) -> Result<Vec<A11yNode>, BrowserError>;
68
69 /// Get the current viewport dimensions.
70 fn get_viewport(&self) -> Result<Viewport, BrowserError>;
71
72 /// Get the current page URL.
73 fn get_current_url(&self) -> Result<String, BrowserError>;
74
75 /// Get the current page title.
76 async fn get_page_title(&self) -> Result<String, BrowserError>;
77
78 // =========================================================================
79 // Navigation
80 // =========================================================================
81
82 /// Navigate to a URL.
83 async fn navigate(&self, url: &str) -> Result<(), BrowserError>;
84
85 // =========================================================================
86 // Human-equivalent input (Manifesto Principle 5)
87 // =========================================================================
88
89 /// Click at viewport coordinates.
90 async fn inject_click(&self, x: f64, y: f64) -> Result<(), BrowserError>;
91
92 /// Type text into the focused element.
93 async fn inject_text(&self, text: &str) -> Result<(), BrowserError>;
94
95 /// Press a key with optional modifiers.
96 async fn inject_keypress(&self, key: &str, modifiers: &[Modifier]) -> Result<(), BrowserError>;
97
98 /// Scroll the page.
99 async fn inject_scroll(&self, delta_y: i32) -> Result<(), BrowserError>;
100
101 /// Insert `text` at the caret, replacing the selection — paste semantics.
102 ///
103 /// Distinct from [`Self::inject_text`], which types character by
104 /// character: a paste is ONE insertion that replaces the selection,
105 /// without firing N keydown handlers a page might read as N keystrokes.
106 /// It exists because a synthesised Cmd+V cannot work — the clipboard
107 /// belongs to the browser, not the page, and an injected key event has no
108 /// access to it, so the host reads its own pasteboard and sends the
109 /// string.
110 ///
111 /// Defaults to typing it, so a backend with no native insertion still
112 /// does the right thing for the user.
113 async fn insert_text(&self, text: &str) -> Result<(), BrowserError> {
114 self.inject_text(text).await
115 }
116
117 // =========================================================================
118 // Accessibility actions (VoiceOver-equivalent)
119 // =========================================================================
120
121 /// Click an element by accessibility node ID (AXPress).
122 async fn click_element(&self, node_id: &str) -> Result<(), BrowserError>;
123
124 /// Type text into an element by accessibility node ID.
125 async fn type_into_element(&self, node_id: &str, text: &str) -> Result<(), BrowserError>;
126
127 /// Focus an element by accessibility node ID.
128 async fn focus_element(&self, node_id: &str) -> Result<(), BrowserError>;
129
130 // =========================================================================
131 // Wait conditions
132 // =========================================================================
133
134 /// Check if page is fully loaded.
135 async fn is_page_loaded(&self) -> Result<bool, BrowserError>;
136
137 /// Wait for a condition to be met.
138 async fn wait_until(
139 &self,
140 condition: &WaitCondition,
141 timeout_ms: u64,
142 ) -> Result<bool, BrowserError>;
143
144 /// Check if an element matching a description exists in the accessibility tree.
145 async fn element_exists_a11y(
146 &self,
147 name_contains: &str,
148 role: Option<&str>,
149 ) -> Result<bool, BrowserError>;
150
151 // =========================================================================
152 // Auth state injection (pre-navigation)
153 // =========================================================================
154
155 /// Inject cookies into the browser. Must be called before navigation
156 /// for the cookies to be sent with the first request.
157 async fn set_cookies(&self, _cookies: &[CookieParam]) -> Result<(), BrowserError> {
158 Err(BrowserError::Unsupported(
159 "set_cookies not implemented".into(),
160 ))
161 }
162
163 /// Set localStorage items for a given origin.
164 /// The browser will briefly navigate to the origin to set the items.
165 async fn set_local_storage(
166 &self,
167 _origin: &str,
168 _items: &[(String, String)],
169 ) -> Result<(), BrowserError> {
170 Err(BrowserError::Unsupported(
171 "set_local_storage not implemented".into(),
172 ))
173 }
174
175 /// Set extra HTTP headers to include on every request.
176 async fn set_extra_headers(&self, _headers: &[(String, String)]) -> Result<(), BrowserError> {
177 Err(BrowserError::Unsupported(
178 "set_extra_headers not implemented".into(),
179 ))
180 }
181
182 // =========================================================================
183 // Lifecycle
184 // =========================================================================
185
186 /// Shut down the browser backend and release resources.
187 ///
188 /// For headless Chromium: terminates the browser process.
189 /// For Tauri: WebView cleanup.
190 /// Default: no-op.
191 async fn shutdown(&self) -> Result<(), BrowserError> {
192 Ok(())
193 }
194}