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() {
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 async_trait::async_trait;
use serde_json::Value;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
struct MockTab {
closed: Arc<AtomicBool>,
}
impl MockTab {
fn new() -> (Self, Arc<AtomicBool>) {
let closed = Arc::new(AtomicBool::new(false));
(
Self {
closed: closed.clone(),
},
closed,
)
}
}
#[async_trait]
impl BrowserTab for MockTab {
async fn goto(&self, _url: &str) -> Result<PageContent, BrowserError> {
Ok(PageContent::empty())
}
async fn click(&self, _selector: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn type_(&self, _selector: &str, _text: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn fill(&self, _selector: &str, _value: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn press(&self, _combo: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn wait_for(&self, _selector: &str, _timeout_ms: u64) -> Result<(), BrowserError> {
Ok(())
}
async fn content(&self) -> Result<PageContent, BrowserError> {
Ok(PageContent::empty())
}
async fn query_all(&self, _selector: &str) -> Result<Vec<String>, BrowserError> {
Ok(vec![])
}
async fn evaluate(&self, _js: &str) -> Result<Value, BrowserError> {
Ok(Value::Null)
}
async fn screenshot(&self, _width: u32) -> Result<Vec<u8>, BrowserError> {
Ok(vec![])
}
async fn close(&self) -> Result<(), BrowserError> {
self.closed.store(true, Ordering::SeqCst);
Ok(())
}
async fn select_option(&self, _selector: &str, _value: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn check(&self, _selector: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn uncheck(&self, _selector: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn clear(&self, _selector: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn hover(&self, _selector: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn double_click(&self, _selector: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn right_click(&self, _selector: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn scroll(&self, _delta_x: f64, _delta_y: f64) -> Result<(), BrowserError> {
Ok(())
}
async fn scroll_into_view(&self, _selector: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn drag(&self, _from: &str, _to: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn upload_file(&self, _selector: &str, _path: &str) -> Result<(), BrowserError> {
Ok(())
}
async fn get_value(&self, _selector: &str) -> Result<String, BrowserError> {
Ok(String::new())
}
async fn evaluate_await(&self, _js: &str) -> Result<Value, BrowserError> {
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;
}
}