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}