browsing 0.1.7

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation
//! File chooser dialog interception via CDP Page domain

use crate::browser::views::FileChooserInfo;
use crate::error::Result;
use std::collections::VecDeque;
use std::sync::Arc;
use tokio::sync::Mutex;

/// Manages file chooser dialog interception
#[derive(Debug, Clone)]
pub struct FileChooserManager {
    client: Arc<crate::browser::cdp::CdpClient>,
    pending: Arc<Mutex<VecDeque<FileChooserInfo>>>,
}

impl FileChooserManager {
    /// Create a new file chooser manager
    pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
        Self {
            client,
            pending: Arc::new(Mutex::new(VecDeque::new())),
        }
    }

    /// Enable file chooser interception.
    /// Subscribes to `Page.fileChooserOpened` events.
    pub async fn enable(&self) -> Result<()> {
        self.client
            .send_command("Page.enable", serde_json::json!({}))
            .await?;
        self.client
            .send_command(
                "Page.setInterceptFileChooserDialog",
                serde_json::json!({ "enabled": true }),
            )
            .await?;

        let pending = Arc::clone(&self.pending);
        let mut rx = self.client.subscribe_events().await?;

        tokio::spawn(async move {
            while let Ok(event) = rx.recv().await {
                if event.method == "Page.fileChooserOpened" {
                    let frame_id = event
                        .params
                        .get("frameId")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_string();
                    let backend_node_id = event
                        .params
                        .get("backendNodeId")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0) as u32;
                    let multiple = event
                        .params
                        .get("multiple")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false);

                    let info = FileChooserInfo {
                        frame_id,
                        backend_node_id,
                        multiple,
                    };
                    pending.lock().await.push_back(info);
                }
            }
        });

        Ok(())
    }

    /// Disable file chooser interception
    pub async fn disable(&self) -> Result<()> {
        self.client
            .send_command(
                "Page.setInterceptFileChooserDialog",
                serde_json::json!({ "enabled": false }),
            )
            .await?;
        self.pending.lock().await.clear();
        Ok(())
    }

    /// Set files on the next pending file chooser using DOM.setFileInputFiles
    pub async fn set_files(&self, paths: Vec<String>) -> Result<()> {
        let info = self
            .pending
            .lock()
            .await
            .pop_front()
            .ok_or_else(|| crate::error::BrowsingError::Browser("No pending file chooser".to_string()))?;

        self.client
            .send_command(
                "DOM.setFileInputFiles",
                serde_json::json!({
                    "files": paths,
                    "backendNodeId": info.backend_node_id,
                }),
            )
            .await?;

        Ok(())
    }

    /// Get all pending file choosers
    pub async fn get_pending(&self) -> Vec<FileChooserInfo> {
        self.pending.lock().await.iter().cloned().collect()
    }

    /// Get the next pending file chooser
    pub async fn next(&self) -> Option<FileChooserInfo> {
        self.pending.lock().await.pop_front()
    }

    /// Check if any file chooser is pending
    pub async fn has_pending(&self) -> bool {
        !self.pending.lock().await.is_empty()
    }

    /// Clear pending chooser records without interacting with the browser
    pub async fn clear(&self) {
        self.pending.lock().await.clear();
    }
}

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

    #[test]
    fn test_file_chooser_info_creation() {
        let info = FileChooserInfo {
            frame_id: "frame-1".to_string(),
            backend_node_id: 42,
            multiple: true,
        };
        assert_eq!(info.frame_id, "frame-1");
        assert_eq!(info.backend_node_id, 42);
        assert!(info.multiple);
    }

    #[test]
    fn test_file_chooser_info_serialization() {
        let info = FileChooserInfo {
            frame_id: "frame-1".to_string(),
            backend_node_id: 42,
            multiple: false,
        };
        let json = serde_json::to_string(&info).unwrap();
        assert!(json.contains("frame-1"));
        assert!(json.contains("42"));
    }
}