viewpoint_core/page/dialog/
mod.rs

1//! Dialog handling for browser dialogs.
2//!
3//! This module provides functionality for handling JavaScript dialogs
4//! (alert, confirm, prompt, beforeunload).
5
6use std::sync::Arc;
7
8use viewpoint_cdp::protocol::{DialogType, HandleJavaScriptDialogParams};
9use viewpoint_cdp::CdpConnection;
10use tracing::{debug, instrument};
11
12use crate::error::PageError;
13
14/// A browser dialog (alert, confirm, prompt, or beforeunload).
15///
16/// Dialogs are emitted via the `page.on_dialog()` callback. You must either
17/// `accept()` or `dismiss()` the dialog - otherwise the page will freeze
18/// waiting for user input.
19///
20/// # Example
21///
22/// ```
23/// # #[cfg(feature = "integration")]
24/// # tokio_test::block_on(async {
25/// # use viewpoint_core::Browser;
26/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
27/// # let context = browser.new_context().await.unwrap();
28/// # let page = context.new_page().await.unwrap();
29///
30/// page.on_dialog(|dialog| async move {
31///     println!("Dialog message: {}", dialog.message());
32///     dialog.accept().await
33/// });
34/// # });
35/// ```
36#[derive(Debug)]
37pub struct Dialog {
38    /// CDP connection.
39    connection: Arc<CdpConnection>,
40    /// Session ID.
41    session_id: String,
42    /// Dialog type.
43    dialog_type: DialogType,
44    /// Dialog message.
45    message: String,
46    /// Default prompt value.
47    default_value: String,
48    /// Whether the dialog has been handled.
49    handled: bool,
50}
51
52impl Dialog {
53    /// Create a new Dialog.
54    pub(crate) fn new(
55        connection: Arc<CdpConnection>,
56        session_id: String,
57        dialog_type: DialogType,
58        message: String,
59        default_value: Option<String>,
60    ) -> Self {
61        Self {
62            connection,
63            session_id,
64            dialog_type,
65            message,
66            default_value: default_value.unwrap_or_default(),
67            handled: false,
68        }
69    }
70
71    /// Get the dialog type.
72    ///
73    /// Returns one of: `alert`, `confirm`, `prompt`, or `beforeunload`.
74    pub fn type_(&self) -> DialogType {
75        self.dialog_type
76    }
77
78    /// Get the dialog message.
79    pub fn message(&self) -> &str {
80        &self.message
81    }
82
83    /// Get the default prompt value.
84    ///
85    /// Only applicable for `prompt` dialogs.
86    pub fn default_value(&self) -> &str {
87        &self.default_value
88    }
89
90    /// Accept the dialog.
91    ///
92    /// For `alert` dialogs, this closes the dialog.
93    /// For `confirm` dialogs, this returns `true` to the JavaScript.
94    /// For `prompt` dialogs, this returns the default value to the JavaScript.
95    /// For `beforeunload` dialogs, this allows navigation to proceed.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the dialog has already been handled or CDP fails.
100    #[instrument(level = "debug", skip(self), fields(dialog_type = %self.dialog_type))]
101    pub async fn accept(self) -> Result<(), PageError> {
102        if self.handled {
103            return Err(PageError::EvaluationFailed(
104                "Dialog has already been handled".to_string(),
105            ));
106        }
107
108        debug!("Accepting dialog");
109
110        self.connection
111            .send_command::<_, serde_json::Value>(
112                "Page.handleJavaScriptDialog",
113                Some(HandleJavaScriptDialogParams {
114                    accept: true,
115                    prompt_text: None,
116                }),
117                Some(&self.session_id),
118            )
119            .await?;
120
121        // Dialog is consumed here - Drop won't run on success
122        // Use forget to prevent Drop from warning about unhandled dialog
123        std::mem::forget(self);
124        Ok(())
125    }
126
127    /// Accept the dialog with the specified text.
128    ///
129    /// This is primarily useful for `prompt` dialogs where you want to
130    /// provide a custom response.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if the dialog has already been handled or CDP fails.
135    #[instrument(level = "debug", skip(self, text), fields(dialog_type = %self.dialog_type))]
136    pub async fn accept_with_text(self, text: impl Into<String>) -> Result<(), PageError> {
137        if self.handled {
138            return Err(PageError::EvaluationFailed(
139                "Dialog has already been handled".to_string(),
140            ));
141        }
142
143        debug!("Accepting dialog with text");
144
145        self.connection
146            .send_command::<_, serde_json::Value>(
147                "Page.handleJavaScriptDialog",
148                Some(HandleJavaScriptDialogParams {
149                    accept: true,
150                    prompt_text: Some(text.into()),
151                }),
152                Some(&self.session_id),
153            )
154            .await?;
155
156        // Dialog is consumed here - Drop won't run on success
157        std::mem::forget(self);
158        Ok(())
159    }
160
161    /// Dismiss the dialog.
162    ///
163    /// For `alert` dialogs, this closes the dialog.
164    /// For `confirm` dialogs, this returns `false` to the JavaScript.
165    /// For `prompt` dialogs, this returns `null` to the JavaScript.
166    /// For `beforeunload` dialogs, this cancels navigation.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if the dialog has already been handled or CDP fails.
171    #[instrument(level = "debug", skip(self), fields(dialog_type = %self.dialog_type))]
172    pub async fn dismiss(self) -> Result<(), PageError> {
173        if self.handled {
174            return Err(PageError::EvaluationFailed(
175                "Dialog has already been handled".to_string(),
176            ));
177        }
178
179        debug!("Dismissing dialog");
180
181        self.connection
182            .send_command::<_, serde_json::Value>(
183                "Page.handleJavaScriptDialog",
184                Some(HandleJavaScriptDialogParams {
185                    accept: false,
186                    prompt_text: None,
187                }),
188                Some(&self.session_id),
189            )
190            .await?;
191
192        // Dialog is consumed here - Drop won't run on success
193        std::mem::forget(self);
194        Ok(())
195    }
196}
197
198impl Drop for Dialog {
199    fn drop(&mut self) {
200        // If dialog wasn't handled, we could log a warning
201        // but we can't auto-dismiss here since we can't do async in Drop
202        if !self.handled {
203            tracing::warn!(
204                "Dialog of type {} was dropped without being handled. This may cause the page to freeze.",
205                self.dialog_type
206            );
207        }
208    }
209}