browsing 0.1.7

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

use crate::browser::views::{DialogInfo, DialogType};
use crate::error::Result;
use std::collections::VecDeque;
use std::sync::Arc;
use tokio::sync::Mutex;

/// Manages JavaScript dialogs (alert, confirm, prompt, beforeunload)
#[derive(Debug, Clone)]
pub struct DialogManager {
    client: Arc<crate::browser::cdp::CdpClient>,
    pending: Arc<Mutex<VecDeque<DialogInfo>>>,
    auto_dismiss: Arc<Mutex<bool>>,
}

impl DialogManager {
    /// Create a new dialog manager
    pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
        Self {
            client,
            pending: Arc::new(Mutex::new(VecDeque::new())),
            auto_dismiss: Arc::new(Mutex::new(false)),
        }
    }

    /// Enable dialog event listening.
    /// Subscribes to `Page.javascriptDialogOpening` and optionally auto-dismisses.
    pub async fn enable(&self) -> Result<()> {
        self.client
            .send_command("Page.enable", serde_json::json!({}))
            .await?;

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

        tokio::spawn(async move {
            while let Ok(event) = rx.recv().await {
                if event.method == "Page.javascriptDialogOpening" {
                    let dialog_type = event
                        .params
                        .get("type")
                        .and_then(|v| v.as_str())
                        .map(parse_dialog_type)
                        .unwrap_or(DialogType::Alert);
                    let message = event
                        .params
                        .get("message")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_string();
                    let default_prompt = event
                        .params
                        .get("defaultPrompt")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string());
                    let url = event
                        .params
                        .get("url")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_string();

                    let info = DialogInfo {
                        dialog_type,
                        message,
                        default_prompt,
                        url,
                    };

                    let should_auto_dismiss = *auto_dismiss.lock().await;
                    if should_auto_dismiss {
                        let _ = client
                            .send_command(
                                "Page.handleJavaScriptDialog",
                                serde_json::json!({
                                    "accept": false,
                                }),
                            )
                            .await;
                    } else {
                        pending.lock().await.push_back(info);
                    }
                }
            }
        });

        Ok(())
    }

    /// Accept a dialog. For prompt dialogs, optional text can be provided.
    pub async fn accept(&self, prompt_text: Option<&str>) -> Result<()> {
        let mut params = serde_json::json!({ "accept": true });
        if let Some(text) = prompt_text {
            params["promptText"] = serde_json::json!(text);
        }
        self.client
            .send_command("Page.handleJavaScriptDialog", params)
            .await?;
        self.pending.lock().await.pop_front();
        Ok(())
    }

    /// Dismiss a dialog (click Cancel / OK for alert)
    pub async fn dismiss(&self) -> Result<()> {
        self.client
            .send_command(
                "Page.handleJavaScriptDialog",
                serde_json::json!({ "accept": false }),
            )
            .await?;
        self.pending.lock().await.pop_front();
        Ok(())
    }

    /// Get all pending dialogs
    pub async fn get_pending_dialogs(&self) -> Vec<DialogInfo> {
        self.pending.lock().await.iter().cloned().collect()
    }

    /// Get the next pending dialog (oldest first)
    pub async fn next_dialog(&self) -> Option<DialogInfo> {
        self.pending.lock().await.pop_front()
    }

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

    /// Enable auto-dismiss mode — all dialogs are automatically dismissed
    pub async fn set_auto_dismiss(&self, enabled: bool) {
        *self.auto_dismiss.lock().await = enabled;
    }

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

fn parse_dialog_type(s: &str) -> DialogType {
    match s {
        "confirm" => DialogType::Confirm,
        "prompt" => DialogType::Prompt,
        "beforeunload" => DialogType::Beforeunload,
        _ => DialogType::Alert,
    }
}

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

    #[test]
    fn test_dialog_type_serialization() {
        let alert = DialogType::Alert;
        let json = serde_json::to_string(&alert).unwrap();
        assert_eq!(json, "\"alert\"");

        let confirm = DialogType::Confirm;
        let json = serde_json::to_string(&confirm).unwrap();
        assert_eq!(json, "\"confirm\"");
    }

    #[test]
    fn test_dialog_info_creation() {
        let info = DialogInfo {
            dialog_type: DialogType::Prompt,
            message: "Enter your name".to_string(),
            default_prompt: Some("John".to_string()),
            url: "https://example.com".to_string(),
        };
        assert_eq!(info.message, "Enter your name");
        assert_eq!(info.default_prompt, Some("John".to_string()));
    }

    #[test]
    fn test_parse_dialog_type() {
        assert!(matches!(parse_dialog_type("alert"), DialogType::Alert));
        assert!(matches!(parse_dialog_type("confirm"), DialogType::Confirm));
        assert!(matches!(parse_dialog_type("prompt"), DialogType::Prompt));
        assert!(matches!(parse_dialog_type("beforeunload"), DialogType::Beforeunload));
        assert!(matches!(parse_dialog_type("unknown"), DialogType::Alert));
    }
}