use super::engine::BrowserTab;
pub struct TabGuard {
tab: Option<Box<dyn BrowserTab>>,
explicitly_consumed: bool,
}
impl TabGuard {
pub fn new(tab: Box<dyn BrowserTab>) -> Self {
Self {
tab: Some(tab),
explicitly_consumed: false,
}
}
pub fn tab(&self) -> &dyn BrowserTab {
self.tab
.as_ref()
.map(|t| t.as_ref() as &dyn BrowserTab)
.expect("TabGuard: tab already consumed")
}
pub async fn close(mut self) {
self.explicitly_consumed = true;
if let Some(tab) = self.tab.take() {
tab.clear_progress_callback();
if let Err(e) = tab.close().await {
tracing::warn!("TabGuard: tab close failed: {}", e);
}
}
}
pub fn into_inner(mut self) -> Box<dyn BrowserTab> {
self.explicitly_consumed = true;
self.tab.take().expect("TabGuard: tab already consumed")
}
}
impl Drop for TabGuard {
fn drop(&mut self) {
if !self.explicitly_consumed {
tracing::warn!(
"TabGuard dropped without explicit close — tab may leak. \
Call .close().await or .into_inner() to prevent this."
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::browse::engine::{BrowserError, PageContent};
use serde_json::Value;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
struct MockTab {
closed: Arc<AtomicBool>,
}
impl MockTab {
fn new<'a>() -> (Self, Arc<AtomicBool>) {
let closed = Arc::new(AtomicBool::new(false));
(
Self {
closed: closed.clone(),
},
closed,
)
}
}
impl BrowserTab for MockTab {
fn goto<'a>(
&'a self,
_url: &str,
) -> Pin<Box<dyn Future<Output = Result<PageContent, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(PageContent::empty()) })
}
fn click<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn type_<'a>(
&'a self,
_selector: &str,
_text: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn fill<'a>(
&'a self,
_selector: &str,
_value: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn press<'a>(
&'a self,
_combo: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn wait_for<'a>(
&'a self,
_selector: &str,
_timeout_ms: u64,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn content<'a>(
&'a self,
) -> Pin<Box<dyn Future<Output = Result<PageContent, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(PageContent::empty()) })
}
fn query_all<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(vec![]) })
}
fn evaluate<'a>(
&'a self,
_js: &str,
) -> Pin<Box<dyn Future<Output = Result<Value, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(Value::Null) })
}
fn screenshot<'a>(
&'a self,
_width: u32,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(vec![]) })
}
fn close<'a>(
&'a self,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move {
self.closed.store(true, Ordering::SeqCst);
Ok(())
})
}
fn back<'a>(
&'a self,
) -> Pin<Box<dyn Future<Output = Result<PageContent, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(PageContent::empty()) })
}
fn forward<'a>(
&'a self,
) -> Pin<Box<dyn Future<Output = Result<PageContent, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(PageContent::empty()) })
}
fn reload<'a>(
&'a self,
) -> Pin<Box<dyn Future<Output = Result<PageContent, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(PageContent::empty()) })
}
fn select_option<'a>(
&'a self,
_selector: &str,
_value: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn check<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn uncheck<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn clear<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn hover<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn double_click<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn right_click<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn scroll<'a>(
&'a self,
_delta_x: f64,
_delta_y: f64,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn scroll_into_view<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn drag<'a>(
&'a self,
_from: &str,
_to: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn upload_file<'a>(
&'a self,
_selector: &str,
_path: &str,
) -> Pin<Box<dyn Future<Output = Result<(), BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
fn get_value<'a>(
&'a self,
_selector: &str,
) -> Pin<Box<dyn Future<Output = Result<String, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(String::new()) })
}
fn evaluate_await<'a>(
&'a self,
_js: &str,
) -> Pin<Box<dyn Future<Output = Result<Value, BrowserError>> + Send + 'a>> {
Box::pin(async move { Ok(Value::Null) })
}
fn is_closed(&self) -> bool {
self.closed.load(Ordering::SeqCst)
}
}
#[tokio::test]
async fn test_guard_close_success() {
let (mock, closed_flag) = MockTab::new();
let guard = TabGuard::new(Box::new(mock));
assert!(!closed_flag.load(Ordering::SeqCst));
guard.close().await;
assert!(closed_flag.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_guard_into_inner() {
let (mock, closed_flag) = MockTab::new();
let guard = TabGuard::new(Box::new(mock));
let _tab = guard.into_inner();
assert!(!closed_flag.load(Ordering::SeqCst));
}
#[test]
fn test_guard_drop_without_close_warns() {
let (mock, _) = MockTab::new();
let guard = TabGuard::new(Box::new(mock));
drop(guard);
}
#[tokio::test]
async fn test_guard_tab_access() {
let (mock, _) = MockTab::new();
let guard = TabGuard::new(Box::new(mock));
let result = guard.tab().goto("https://example.com").await;
assert!(result.is_ok());
guard.close().await;
}
}