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}