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/// ```ignore
23/// page.on_dialog(|dialog| async move {
24///     println!("Dialog message: {}", dialog.message());
25///     dialog.accept().await
26/// });
27/// ```
28#[derive(Debug)]
29pub struct Dialog {
30    /// CDP connection.
31    connection: Arc<CdpConnection>,
32    /// Session ID.
33    session_id: String,
34    /// Dialog type.
35    dialog_type: DialogType,
36    /// Dialog message.
37    message: String,
38    /// Default prompt value.
39    default_value: String,
40    /// Whether the dialog has been handled.
41    handled: bool,
42}
43
44impl Dialog {
45    /// Create a new Dialog.
46    pub(crate) fn new(
47        connection: Arc<CdpConnection>,
48        session_id: String,
49        dialog_type: DialogType,
50        message: String,
51        default_value: Option<String>,
52    ) -> Self {
53        Self {
54            connection,
55            session_id,
56            dialog_type,
57            message,
58            default_value: default_value.unwrap_or_default(),
59            handled: false,
60        }
61    }
62
63    /// Get the dialog type.
64    ///
65    /// Returns one of: `alert`, `confirm`, `prompt`, or `beforeunload`.
66    pub fn type_(&self) -> DialogType {
67        self.dialog_type
68    }
69
70    /// Get the dialog message.
71    pub fn message(&self) -> &str {
72        &self.message
73    }
74
75    /// Get the default prompt value.
76    ///
77    /// Only applicable for `prompt` dialogs.
78    pub fn default_value(&self) -> &str {
79        &self.default_value
80    }
81
82    /// Accept the dialog.
83    ///
84    /// For `alert` dialogs, this closes the dialog.
85    /// For `confirm` dialogs, this returns `true` to the JavaScript.
86    /// For `prompt` dialogs, this returns the default value to the JavaScript.
87    /// For `beforeunload` dialogs, this allows navigation to proceed.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if the dialog has already been handled or CDP fails.
92    #[instrument(level = "debug", skip(self), fields(dialog_type = %self.dialog_type))]
93    pub async fn accept(self) -> Result<(), PageError> {
94        if self.handled {
95            return Err(PageError::EvaluationFailed(
96                "Dialog has already been handled".to_string(),
97            ));
98        }
99
100        debug!("Accepting dialog");
101
102        self.connection
103            .send_command::<_, serde_json::Value>(
104                "Page.handleJavaScriptDialog",
105                Some(HandleJavaScriptDialogParams {
106                    accept: true,
107                    prompt_text: None,
108                }),
109                Some(&self.session_id),
110            )
111            .await?;
112
113        // Dialog is consumed here - Drop won't run on success
114        // Use forget to prevent Drop from warning about unhandled dialog
115        std::mem::forget(self);
116        Ok(())
117    }
118
119    /// Accept the dialog with the specified text.
120    ///
121    /// This is primarily useful for `prompt` dialogs where you want to
122    /// provide a custom response.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the dialog has already been handled or CDP fails.
127    #[instrument(level = "debug", skip(self, text), fields(dialog_type = %self.dialog_type))]
128    pub async fn accept_with_text(self, text: impl Into<String>) -> Result<(), PageError> {
129        if self.handled {
130            return Err(PageError::EvaluationFailed(
131                "Dialog has already been handled".to_string(),
132            ));
133        }
134
135        debug!("Accepting dialog with text");
136
137        self.connection
138            .send_command::<_, serde_json::Value>(
139                "Page.handleJavaScriptDialog",
140                Some(HandleJavaScriptDialogParams {
141                    accept: true,
142                    prompt_text: Some(text.into()),
143                }),
144                Some(&self.session_id),
145            )
146            .await?;
147
148        // Dialog is consumed here - Drop won't run on success
149        std::mem::forget(self);
150        Ok(())
151    }
152
153    /// Dismiss the dialog.
154    ///
155    /// For `alert` dialogs, this closes the dialog.
156    /// For `confirm` dialogs, this returns `false` to the JavaScript.
157    /// For `prompt` dialogs, this returns `null` to the JavaScript.
158    /// For `beforeunload` dialogs, this cancels navigation.
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 dismiss(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!("Dismissing dialog");
172
173        self.connection
174            .send_command::<_, serde_json::Value>(
175                "Page.handleJavaScriptDialog",
176                Some(HandleJavaScriptDialogParams {
177                    accept: false,
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        std::mem::forget(self);
186        Ok(())
187    }
188}
189
190impl Drop for Dialog {
191    fn drop(&mut self) {
192        // If dialog wasn't handled, we could log a warning
193        // but we can't auto-dismiss here since we can't do async in Drop
194        if !self.handled {
195            tracing::warn!(
196                "Dialog of type {} was dropped without being handled. This may cause the page to freeze.",
197                self.dialog_type
198            );
199        }
200    }
201}