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}