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//!
6//! # Intercepting and Responding to Dialogs
7//!
8//! Browser dialogs block page execution until handled. Use `page.on_dialog()`
9//! to intercept dialogs and respond with accept or dismiss:
10//!
11//! ```ignore
12//! use viewpoint_core::{Browser, Dialog, DialogType};
13//!
14//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
15//! let browser = Browser::launch().headless(true).launch().await?;
16//! let context = browser.new_context().await?;
17//! let page = context.new_page().await?;
18//!
19//! // Handle all dialogs by accepting them
20//! page.on_dialog(|dialog| async move {
21//!     match dialog.type_() {
22//!         DialogType::Alert => {
23//!             println!("Alert: {}", dialog.message());
24//!             dialog.accept().await
25//!         }
26//!         DialogType::Confirm => {
27//!             println!("Confirm: {}", dialog.message());
28//!             dialog.accept().await  // Click "OK"
29//!         }
30//!         DialogType::Prompt => {
31//!             println!("Prompt: {}", dialog.message());
32//!             // Respond with custom text
33//!             dialog.accept_with_text("my response").await
34//!         }
35//!         DialogType::Beforeunload => {
36//!             dialog.dismiss().await  // Stay on page
37//!         }
38//!     }
39//! }).await;
40//!
41//! page.goto("https://example.com").goto().await?;
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # Dismissing Dialogs
47//!
48//! To dismiss (cancel) a dialog instead of accepting it:
49//!
50//! ```ignore
51//! page.on_dialog(|dialog| async move {
52//!     dialog.dismiss().await  // Click "Cancel" or dismiss
53//! }).await;
54//! ```
55//!
56//! # Responding to Prompt Dialogs with Custom Values
57//!
58//! ```ignore
59//! page.on_dialog(|dialog| async move {
60//!     if matches!(dialog.type_(), DialogType::Prompt) {
61//!         // Enter custom text in the prompt
62//!         dialog.accept_with_text("custom value").await
63//!     } else {
64//!         dialog.accept().await
65//!     }
66//! }).await;
67//! ```
68
69use std::sync::Arc;
70
71use tracing::{debug, instrument};
72use viewpoint_cdp::CdpConnection;
73use viewpoint_cdp::protocol::{DialogType, HandleJavaScriptDialogParams};
74
75use crate::error::PageError;
76
77/// A browser dialog (alert, confirm, prompt, or beforeunload).
78///
79/// Dialogs are emitted via the `page.on_dialog()` callback. You must either
80/// `accept()` or `dismiss()` the dialog - otherwise the page will freeze
81/// waiting for user input.
82///
83/// # Example
84///
85/// ```
86/// # #[cfg(feature = "integration")]
87/// # tokio_test::block_on(async {
88/// # use viewpoint_core::Browser;
89/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
90/// # let context = browser.new_context().await.unwrap();
91/// # let page = context.new_page().await.unwrap();
92///
93/// page.on_dialog(|dialog| async move {
94///     println!("Dialog message: {}", dialog.message());
95///     dialog.accept().await
96/// });
97/// # });
98/// ```
99#[derive(Debug)]
100pub struct Dialog {
101    /// CDP connection.
102    connection: Arc<CdpConnection>,
103    /// Session ID.
104    session_id: String,
105    /// Dialog type.
106    dialog_type: DialogType,
107    /// Dialog message.
108    message: String,
109    /// Default prompt value.
110    default_value: String,
111    /// Whether the dialog has been handled.
112    handled: bool,
113}
114
115impl Dialog {
116    /// Create a new Dialog.
117    pub(crate) fn new(
118        connection: Arc<CdpConnection>,
119        session_id: String,
120        dialog_type: DialogType,
121        message: String,
122        default_value: Option<String>,
123    ) -> Self {
124        Self {
125            connection,
126            session_id,
127            dialog_type,
128            message,
129            default_value: default_value.unwrap_or_default(),
130            handled: false,
131        }
132    }
133
134    /// Get the dialog type.
135    ///
136    /// Returns one of: `alert`, `confirm`, `prompt`, or `beforeunload`.
137    pub fn type_(&self) -> DialogType {
138        self.dialog_type
139    }
140
141    /// Get the dialog message.
142    pub fn message(&self) -> &str {
143        &self.message
144    }
145
146    /// Get the default prompt value.
147    ///
148    /// Only applicable for `prompt` dialogs.
149    pub fn default_value(&self) -> &str {
150        &self.default_value
151    }
152
153    /// Accept the dialog.
154    ///
155    /// For `alert` dialogs, this closes the dialog.
156    /// For `confirm` dialogs, this returns `true` to the JavaScript.
157    /// For `prompt` dialogs, this returns the default value to the JavaScript.
158    /// For `beforeunload` dialogs, this allows navigation to proceed.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the dialog has already been handled or CDP fails.
163    #[instrument(level = "debug", skip(self), fields(dialog_type = %self.dialog_type))]
164    pub async fn accept(self) -> Result<(), PageError> {
165        if self.handled {
166            return Err(PageError::EvaluationFailed(
167                "Dialog has already been handled".to_string(),
168            ));
169        }
170
171        debug!("Accepting dialog");
172
173        self.connection
174            .send_command::<_, serde_json::Value>(
175                "Page.handleJavaScriptDialog",
176                Some(HandleJavaScriptDialogParams {
177                    accept: true,
178                    prompt_text: None,
179                }),
180                Some(&self.session_id),
181            )
182            .await?;
183
184        // Dialog is consumed here - Drop won't run on success
185        // Use forget to prevent Drop from warning about unhandled dialog
186        std::mem::forget(self);
187        Ok(())
188    }
189
190    /// Accept the dialog with the specified text.
191    ///
192    /// This is primarily useful for `prompt` dialogs where you want to
193    /// provide a custom response.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if the dialog has already been handled or CDP fails.
198    #[instrument(level = "debug", skip(self, text), fields(dialog_type = %self.dialog_type))]
199    pub async fn accept_with_text(self, text: impl Into<String>) -> Result<(), PageError> {
200        if self.handled {
201            return Err(PageError::EvaluationFailed(
202                "Dialog has already been handled".to_string(),
203            ));
204        }
205
206        debug!("Accepting dialog with text");
207
208        self.connection
209            .send_command::<_, serde_json::Value>(
210                "Page.handleJavaScriptDialog",
211                Some(HandleJavaScriptDialogParams {
212                    accept: true,
213                    prompt_text: Some(text.into()),
214                }),
215                Some(&self.session_id),
216            )
217            .await?;
218
219        // Dialog is consumed here - Drop won't run on success
220        std::mem::forget(self);
221        Ok(())
222    }
223
224    /// Dismiss the dialog.
225    ///
226    /// For `alert` dialogs, this closes the dialog.
227    /// For `confirm` dialogs, this returns `false` to the JavaScript.
228    /// For `prompt` dialogs, this returns `null` to the JavaScript.
229    /// For `beforeunload` dialogs, this cancels navigation.
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if the dialog has already been handled or CDP fails.
234    #[instrument(level = "debug", skip(self), fields(dialog_type = %self.dialog_type))]
235    pub async fn dismiss(self) -> Result<(), PageError> {
236        if self.handled {
237            return Err(PageError::EvaluationFailed(
238                "Dialog has already been handled".to_string(),
239            ));
240        }
241
242        debug!("Dismissing dialog");
243
244        self.connection
245            .send_command::<_, serde_json::Value>(
246                "Page.handleJavaScriptDialog",
247                Some(HandleJavaScriptDialogParams {
248                    accept: false,
249                    prompt_text: None,
250                }),
251                Some(&self.session_id),
252            )
253            .await?;
254
255        // Dialog is consumed here - Drop won't run on success
256        std::mem::forget(self);
257        Ok(())
258    }
259}
260
261impl Drop for Dialog {
262    fn drop(&mut self) {
263        // If dialog wasn't handled, we could log a warning
264        // but we can't auto-dismiss here since we can't do async in Drop
265        if !self.handled {
266            tracing::warn!(
267                "Dialog of type {} was dropped without being handled. This may cause the page to freeze.",
268                self.dialog_type
269            );
270        }
271    }
272}