oxicode_agent/tools/browse/
tab_guard.rs1use super::engine::BrowserTab;
8
9pub struct TabGuard {
28 tab: Option<Box<dyn BrowserTab>>,
29 explicitly_consumed: bool,
30}
31
32impl TabGuard {
33 pub fn new(tab: Box<dyn BrowserTab>) -> Self {
35 Self {
36 tab: Some(tab),
37 explicitly_consumed: false,
38 }
39 }
40
41 pub fn tab(&self) -> &dyn BrowserTab {
47 #[allow(clippy::expect_used)]
51 self.tab
52 .as_ref()
53 .map(|t| t.as_ref() as &dyn BrowserTab)
54 .expect("TabGuard: tab already consumed")
55 }
56
57 pub async fn close(mut self) {
62 self.explicitly_consumed = true;
63 if let Some(tab) = self.tab.take() {
64 tab.clear_progress_callback();
65 if let Err(e) = tab.close().await {
66 tracing::warn!("TabGuard: tab close failed: {}", e);
67 }
68 }
69 }
70
71 pub fn into_inner(mut self) -> Box<dyn BrowserTab> {
76 self.explicitly_consumed = true;
77 #[allow(clippy::expect_used)]
80 self.tab.take().expect("TabGuard: tab already consumed")
81 }
82}
83
84impl Drop for TabGuard {
85 fn drop(&mut self) {
86 if !self.explicitly_consumed {
87 tracing::warn!(
88 "TabGuard dropped without explicit close — tab may leak. \
89 Call .close().await or .into_inner() to prevent this."
90 );
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::tools::browse::engine::{BrowserError, PageContent};
99 use async_trait::async_trait;
100 use serde_json::Value;
101 use std::sync::Arc;
102 use std::sync::atomic::{AtomicBool, Ordering};
103
104 struct MockTab {
107 closed: Arc<AtomicBool>,
108 }
109
110 impl MockTab {
111 fn new() -> (Self, Arc<AtomicBool>) {
112 let closed = Arc::new(AtomicBool::new(false));
113 (
114 Self {
115 closed: closed.clone(),
116 },
117 closed,
118 )
119 }
120 }
121
122 #[async_trait]
123 impl BrowserTab for MockTab {
124 async fn goto(&self, _url: &str) -> Result<PageContent, BrowserError> {
125 Ok(PageContent::empty())
126 }
127 async fn click(&self, _selector: &str) -> Result<(), BrowserError> {
128 Ok(())
129 }
130 async fn type_(&self, _selector: &str, _text: &str) -> Result<(), BrowserError> {
131 Ok(())
132 }
133 async fn fill(&self, _selector: &str, _value: &str) -> Result<(), BrowserError> {
134 Ok(())
135 }
136 async fn press(&self, _combo: &str) -> Result<(), BrowserError> {
137 Ok(())
138 }
139 async fn wait_for(&self, _selector: &str, _timeout_ms: u64) -> Result<(), BrowserError> {
140 Ok(())
141 }
142 async fn content(&self) -> Result<PageContent, BrowserError> {
143 Ok(PageContent::empty())
144 }
145 async fn query_all(&self, _selector: &str) -> Result<Vec<String>, BrowserError> {
146 Ok(vec![])
147 }
148 async fn evaluate(&self, _js: &str) -> Result<Value, BrowserError> {
149 Ok(Value::Null)
150 }
151 async fn screenshot(&self, _width: u32) -> Result<Vec<u8>, BrowserError> {
152 Ok(vec![])
153 }
154 async fn close(&self) -> Result<(), BrowserError> {
155 self.closed.store(true, Ordering::SeqCst);
156 Ok(())
157 }
158 async fn back(&self) -> Result<PageContent, BrowserError> {
159 Ok(PageContent::empty())
160 }
161 async fn forward(&self) -> Result<PageContent, BrowserError> {
162 Ok(PageContent::empty())
163 }
164 async fn reload(&self) -> Result<PageContent, BrowserError> {
165 Ok(PageContent::empty())
166 }
167 async fn select_option(&self, _selector: &str, _value: &str) -> Result<(), BrowserError> {
168 Ok(())
169 }
170 async fn check(&self, _selector: &str) -> Result<(), BrowserError> {
171 Ok(())
172 }
173 async fn uncheck(&self, _selector: &str) -> Result<(), BrowserError> {
174 Ok(())
175 }
176 async fn clear(&self, _selector: &str) -> Result<(), BrowserError> {
177 Ok(())
178 }
179 async fn hover(&self, _selector: &str) -> Result<(), BrowserError> {
180 Ok(())
181 }
182 async fn double_click(&self, _selector: &str) -> Result<(), BrowserError> {
183 Ok(())
184 }
185 async fn right_click(&self, _selector: &str) -> Result<(), BrowserError> {
186 Ok(())
187 }
188 async fn scroll(&self, _delta_x: f64, _delta_y: f64) -> Result<(), BrowserError> {
189 Ok(())
190 }
191 async fn scroll_into_view(&self, _selector: &str) -> Result<(), BrowserError> {
192 Ok(())
193 }
194 async fn drag(&self, _from: &str, _to: &str) -> Result<(), BrowserError> {
195 Ok(())
196 }
197 async fn upload_file(&self, _selector: &str, _path: &str) -> Result<(), BrowserError> {
198 Ok(())
199 }
200 async fn get_value(&self, _selector: &str) -> Result<String, BrowserError> {
201 Ok(String::new())
202 }
203 async fn evaluate_await(&self, _js: &str) -> Result<Value, BrowserError> {
204 Ok(Value::Null)
205 }
206 fn is_closed(&self) -> bool {
207 self.closed.load(Ordering::SeqCst)
208 }
209 }
210
211 #[tokio::test]
214 async fn test_guard_close_success() {
215 let (mock, closed_flag) = MockTab::new();
216 let guard = TabGuard::new(Box::new(mock));
217 assert!(!closed_flag.load(Ordering::SeqCst));
218 guard.close().await;
219 assert!(closed_flag.load(Ordering::SeqCst));
220 }
221
222 #[tokio::test]
223 async fn test_guard_into_inner() {
224 let (mock, closed_flag) = MockTab::new();
225 let guard = TabGuard::new(Box::new(mock));
226 let _tab = guard.into_inner();
227 assert!(!closed_flag.load(Ordering::SeqCst));
229 }
230
231 #[test]
232 fn test_guard_drop_without_close_warns() {
233 let (mock, _) = MockTab::new();
234 let guard = TabGuard::new(Box::new(mock));
235 drop(guard);
237 }
238
239 #[tokio::test]
240 async fn test_guard_tab_access() {
241 let (mock, _) = MockTab::new();
242 let guard = TabGuard::new(Box::new(mock));
243 let result = guard.tab().goto("https://example.com").await;
245 assert!(result.is_ok());
246 guard.close().await;
247 }
248}