browsing 0.1.7

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation
//! Download handling via CDP Page domain

use crate::browser::views::{DownloadInfo, DownloadState};
use crate::error::Result;
use std::sync::Arc;
use tokio::sync::Mutex;

/// Manages browser download behavior and tracks downloads
#[derive(Debug, Clone)]
pub struct DownloadManager {
    client: Arc<crate::browser::cdp::CdpClient>,
    downloads: Arc<Mutex<Vec<DownloadInfo>>>,
}

impl DownloadManager {
    /// Create a new download manager
    pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
        Self {
            client,
            downloads: Arc::new(Mutex::new(Vec::new())),
        }
    }

    /// Set download behavior — configure where downloads are saved.
    /// `path` must be an absolute directory path.
    /// `behavior` can be `"allow"` (save to path) or `"default"` (prompt).
    pub async fn set_download_behavior(&self, path: &str, behavior: &str) -> Result<()> {
        self.client
            .send_command(
                "Page.setDownloadBehavior",
                serde_json::json!({
                    "behavior": behavior,
                    "downloadPath": path,
                }),
            )
            .await?;
        Ok(())
    }

    /// Enable download event tracking.
    /// Call this once to start collecting `Page.downloadWillBegin` and `Page.downloadProgress` events.
    pub async fn start_tracking(&self) -> Result<()> {
        let downloads = Arc::clone(&self.downloads);
        let mut rx = self.client.subscribe_events().await?;

        tokio::spawn(async move {
            while let Ok(event) = rx.recv().await {
                match event.method.as_str() {
                    "Page.downloadWillBegin" => {
                        if let Some(url) = event.params.get("url").and_then(|v| v.as_str()) {
                            let info = DownloadInfo {
                                url: url.to_string(),
                                filename: event
                                    .params
                                    .get("suggestedFilename")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("")
                                    .to_string(),
                                guid: event
                                    .params
                                    .get("guid")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("")
                                    .to_string(),
                                total_bytes: None,
                                received_bytes: None,
                                state: Some(DownloadState::InProgress),
                            };
                            let mut guard = downloads.lock().await;
                            guard.push(info);
                        }
                    }
                    "Page.downloadProgress" => {
                        let guid = event
                            .params
                            .get("guid")
                            .and_then(|v| v.as_str())
                            .unwrap_or("");
                        let state_str = event
                            .params
                            .get("state")
                            .and_then(|v| v.as_str())
                            .unwrap_or("");
                        let received = event
                            .params
                            .get("receivedBytes")
                            .and_then(|v| v.as_u64());
                        let total = event
                            .params
                            .get("totalBytes")
                            .and_then(|v| v.as_u64());

                        let mut guard = downloads.lock().await;
                        if let Some(d) = guard.iter_mut().find(|d| d.guid == guid) {
                            d.received_bytes = received;
                            d.total_bytes = total;
                            d.state = match state_str {
                                "completed" => Some(DownloadState::Completed),
                                "canceled" => Some(DownloadState::Cancelled),
                                _ => Some(DownloadState::InProgress),
                            };
                        }
                    }
                    _ => {}
                }
            }
        });

        Ok(())
    }

    /// Get a snapshot of all tracked downloads
    pub async fn get_downloads(&self) -> Vec<DownloadInfo> {
        self.downloads.lock().await.clone()
    }

    /// Get only in-progress downloads
    pub async fn get_active_downloads(&self) -> Vec<DownloadInfo> {
        let guard = self.downloads.lock().await;
        guard
            .iter()
            .filter(|d| matches!(d.state, Some(DownloadState::InProgress)))
            .cloned()
            .collect()
    }

    /// Clear the download tracking list
    pub async fn clear_downloads(&self) {
        self.downloads.lock().await.clear();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_download_state_serialization() {
        let state = DownloadState::Completed;
        let json = serde_json::to_string(&state).unwrap();
        assert_eq!(json, "\"completed\"");
    }

    #[test]
    fn test_download_info_creation() {
        let info = DownloadInfo {
            url: "https://example.com/file.zip".to_string(),
            filename: "file.zip".to_string(),
            guid: "abc-123".to_string(),
            total_bytes: Some(1024),
            received_bytes: Some(512),
            state: Some(DownloadState::InProgress),
        };
        assert_eq!(info.url, "https://example.com/file.zip");
        assert_eq!(info.guid, "abc-123");
    }

    #[test]
    fn test_download_info_serialization() {
        let info = DownloadInfo {
            url: "https://example.com/file.pdf".to_string(),
            filename: "file.pdf".to_string(),
            guid: "guid-1".to_string(),
            total_bytes: None,
            received_bytes: None,
            state: Some(DownloadState::Cancelled),
        };
        let json = serde_json::to_string(&info).unwrap();
        assert!(json.contains("file.pdf"));
        assert!(json.contains("cancelled"));
    }
}