Skip to main content

browser_commander/core/
dialog.rs

1//! Dialog event handling for browser automation.
2//!
3//! This module provides a unified dialog manager that handles browser dialogs
4//! (alert, confirm, prompt, beforeunload) across different browser engines.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use browser_commander::core::dialog::{DialogEvent, DialogManager};
10//!
11//! let mut manager = DialogManager::new();
12//!
13//! // Register a handler that dismisses all dialogs
14//! manager.on_dialog(|event| {
15//!     // In real usage, you would call event.dismiss() on the actual dialog object
16//!     println!("Dialog: {} - {}", event.dialog_type, event.message);
17//! });
18//! ```
19
20use crate::core::engine::EngineType;
21use std::fmt;
22use std::sync::{Arc, Mutex};
23
24/// The type of browser dialog.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum DialogType {
27    /// JavaScript alert() dialog.
28    Alert,
29    /// JavaScript confirm() dialog.
30    Confirm,
31    /// JavaScript prompt() dialog.
32    Prompt,
33    /// Page beforeunload dialog.
34    BeforeUnload,
35    /// Unknown dialog type.
36    Unknown,
37}
38
39impl fmt::Display for DialogType {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            DialogType::Alert => write!(f, "alert"),
43            DialogType::Confirm => write!(f, "confirm"),
44            DialogType::Prompt => write!(f, "prompt"),
45            DialogType::BeforeUnload => write!(f, "beforeunload"),
46            DialogType::Unknown => write!(f, "unknown"),
47        }
48    }
49}
50
51impl From<&str> for DialogType {
52    fn from(s: &str) -> Self {
53        match s.to_lowercase().as_str() {
54            "alert" => DialogType::Alert,
55            "confirm" => DialogType::Confirm,
56            "prompt" => DialogType::Prompt,
57            "beforeunload" => DialogType::BeforeUnload,
58            _ => DialogType::Unknown,
59        }
60    }
61}
62
63/// Information about a dialog event.
64///
65/// This struct is passed to dialog handlers and contains all relevant
66/// information about the dialog that was triggered.
67#[derive(Debug, Clone)]
68pub struct DialogEvent {
69    /// The type of dialog.
70    pub dialog_type: DialogType,
71    /// The message shown in the dialog.
72    pub message: String,
73    /// Default value for prompt dialogs.
74    pub default_value: Option<String>,
75}
76
77impl DialogEvent {
78    /// Create a new dialog event.
79    pub fn new(
80        dialog_type: DialogType,
81        message: impl Into<String>,
82        default_value: Option<String>,
83    ) -> Self {
84        Self {
85            dialog_type,
86            message: message.into(),
87            default_value,
88        }
89    }
90
91    /// Create a dialog event from a string type name.
92    pub fn from_str_type(
93        dialog_type: &str,
94        message: impl Into<String>,
95        default_value: Option<String>,
96    ) -> Self {
97        Self::new(DialogType::from(dialog_type), message, default_value)
98    }
99}
100
101/// A type alias for dialog handler functions.
102pub type DialogHandler = Arc<dyn Fn(&DialogEvent) + Send + Sync>;
103
104/// Manages dialog event handlers for browser automation.
105///
106/// The `DialogManager` allows registering callbacks that are invoked
107/// whenever a browser dialog (alert, confirm, prompt) is triggered.
108/// It is engine-agnostic: the engine-specific dialog interception
109/// logic calls [`DialogManager::dispatch`] when a dialog event occurs.
110///
111/// # Thread Safety
112///
113/// `DialogManager` is thread-safe and can be shared across async tasks.
114///
115/// # Example
116///
117/// ```rust
118/// use browser_commander::core::dialog::{DialogEvent, DialogManager};
119///
120/// let mut manager = DialogManager::new();
121///
122/// manager.on_dialog(|event| {
123///     println!("Got dialog: {:?} - {}", event.dialog_type, event.message);
124/// });
125///
126/// // Simulate dispatching a dialog (engine would call this internally)
127/// let event = DialogEvent::from_str_type("alert", "Hello!", None);
128/// manager.dispatch(&event);
129/// ```
130#[derive(Clone)]
131pub struct DialogManager {
132    handlers: Arc<Mutex<Vec<DialogHandler>>>,
133    engine: EngineType,
134}
135
136impl fmt::Debug for DialogManager {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        let count = self.handlers.lock().map(|h| h.len()).unwrap_or(0);
139        f.debug_struct("DialogManager")
140            .field("engine", &self.engine)
141            .field("handler_count", &count)
142            .finish()
143    }
144}
145
146impl DialogManager {
147    /// Create a new `DialogManager` for the given engine.
148    pub fn new_with_engine(engine: EngineType) -> Self {
149        Self {
150            handlers: Arc::new(Mutex::new(Vec::new())),
151            engine,
152        }
153    }
154
155    /// Create a new `DialogManager` (engine-independent, for testing/simple use).
156    pub fn new() -> Self {
157        Self::new_with_engine(EngineType::Chromiumoxide)
158    }
159
160    /// Register a dialog handler.
161    ///
162    /// The handler is called synchronously whenever a dialog event is dispatched.
163    /// For async handling, dispatch a channel message or use `tokio::spawn` inside
164    /// the handler.
165    ///
166    /// # Arguments
167    ///
168    /// * `handler` - A function that receives a [`DialogEvent`] reference.
169    pub fn on_dialog<F>(&mut self, handler: F)
170    where
171        F: Fn(&DialogEvent) + Send + Sync + 'static,
172    {
173        let mut handlers = self.handlers.lock().expect("lock poisoned");
174        handlers.push(Arc::new(handler));
175    }
176
177    /// Remove all registered dialog handlers.
178    pub fn clear_dialog_handlers(&mut self) {
179        let mut handlers = self.handlers.lock().expect("lock poisoned");
180        handlers.clear();
181    }
182
183    /// Get the number of registered handlers.
184    pub fn handler_count(&self) -> usize {
185        self.handlers.lock().map(|h| h.len()).unwrap_or(0)
186    }
187
188    /// Dispatch a dialog event to all registered handlers.
189    ///
190    /// This is called by the engine-specific integration when a dialog appears.
191    /// If no handlers are registered, this is a no-op (the engine integration
192    /// should auto-dismiss the dialog in that case).
193    ///
194    /// # Arguments
195    ///
196    /// * `event` - The dialog event to dispatch.
197    pub fn dispatch(&self, event: &DialogEvent) {
198        let handlers = self.handlers.lock().expect("lock poisoned");
199        for handler in handlers.iter() {
200            handler(event);
201        }
202    }
203
204    /// Get the engine type this manager is associated with.
205    pub fn engine(&self) -> EngineType {
206        self.engine
207    }
208}
209
210impl Default for DialogManager {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn dialog_type_from_str() {
222        assert_eq!(DialogType::from("alert"), DialogType::Alert);
223        assert_eq!(DialogType::from("confirm"), DialogType::Confirm);
224        assert_eq!(DialogType::from("prompt"), DialogType::Prompt);
225        assert_eq!(DialogType::from("beforeunload"), DialogType::BeforeUnload);
226        assert_eq!(DialogType::from("unknown_type"), DialogType::Unknown);
227    }
228
229    #[test]
230    fn dialog_type_display() {
231        assert_eq!(DialogType::Alert.to_string(), "alert");
232        assert_eq!(DialogType::Confirm.to_string(), "confirm");
233        assert_eq!(DialogType::Prompt.to_string(), "prompt");
234        assert_eq!(DialogType::BeforeUnload.to_string(), "beforeunload");
235        assert_eq!(DialogType::Unknown.to_string(), "unknown");
236    }
237
238    #[test]
239    fn dialog_type_case_insensitive() {
240        assert_eq!(DialogType::from("ALERT"), DialogType::Alert);
241        assert_eq!(DialogType::from("Confirm"), DialogType::Confirm);
242    }
243
244    #[test]
245    fn dialog_event_new() {
246        let event = DialogEvent::new(DialogType::Alert, "Hello!", None);
247        assert_eq!(event.dialog_type, DialogType::Alert);
248        assert_eq!(event.message, "Hello!");
249        assert!(event.default_value.is_none());
250    }
251
252    #[test]
253    fn dialog_event_from_str_type() {
254        let event = DialogEvent::from_str_type("confirm", "Are you sure?", None);
255        assert_eq!(event.dialog_type, DialogType::Confirm);
256        assert_eq!(event.message, "Are you sure?");
257    }
258
259    #[test]
260    fn dialog_event_with_default_value() {
261        let event = DialogEvent::new(
262            DialogType::Prompt,
263            "Enter name:",
264            Some("default".to_string()),
265        );
266        assert_eq!(event.dialog_type, DialogType::Prompt);
267        assert_eq!(event.default_value, Some("default".to_string()));
268    }
269
270    #[test]
271    fn dialog_manager_new() {
272        let manager = DialogManager::new();
273        assert_eq!(manager.handler_count(), 0);
274    }
275
276    #[test]
277    fn dialog_manager_on_dialog() {
278        let mut manager = DialogManager::new();
279        manager.on_dialog(|_event| {});
280        assert_eq!(manager.handler_count(), 1);
281
282        manager.on_dialog(|_event| {});
283        assert_eq!(manager.handler_count(), 2);
284    }
285
286    #[test]
287    fn dialog_manager_clear_handlers() {
288        let mut manager = DialogManager::new();
289        manager.on_dialog(|_event| {});
290        manager.on_dialog(|_event| {});
291        assert_eq!(manager.handler_count(), 2);
292
293        manager.clear_dialog_handlers();
294        assert_eq!(manager.handler_count(), 0);
295    }
296
297    #[test]
298    fn dialog_manager_dispatch_calls_handlers() {
299        let mut manager = DialogManager::new();
300        let called = Arc::new(Mutex::new(false));
301        let called_clone = called.clone();
302
303        manager.on_dialog(move |event| {
304            *called_clone.lock().unwrap() = true;
305            assert_eq!(event.message, "Test dialog");
306            assert_eq!(event.dialog_type, DialogType::Alert);
307        });
308
309        let event = DialogEvent::from_str_type("alert", "Test dialog", None);
310        manager.dispatch(&event);
311
312        assert!(*called.lock().unwrap());
313    }
314
315    #[test]
316    fn dialog_manager_dispatch_calls_multiple_handlers() {
317        let mut manager = DialogManager::new();
318        let count = Arc::new(Mutex::new(0u32));
319
320        let count1 = count.clone();
321        manager.on_dialog(move |_| {
322            *count1.lock().unwrap() += 1;
323        });
324
325        let count2 = count.clone();
326        manager.on_dialog(move |_| {
327            *count2.lock().unwrap() += 1;
328        });
329
330        let event = DialogEvent::from_str_type("confirm", "Are you sure?", None);
331        manager.dispatch(&event);
332
333        assert_eq!(*count.lock().unwrap(), 2);
334    }
335
336    #[test]
337    fn dialog_manager_dispatch_with_no_handlers() {
338        let manager = DialogManager::new();
339        // Should not panic with no handlers
340        let event = DialogEvent::from_str_type("alert", "Hello!", None);
341        manager.dispatch(&event);
342    }
343
344    #[test]
345    fn dialog_manager_is_cloneable() {
346        let mut manager = DialogManager::new();
347        manager.on_dialog(|_| {});
348
349        let cloned = manager.clone();
350        assert_eq!(cloned.handler_count(), 1);
351    }
352
353    #[test]
354    fn dialog_manager_engine() {
355        let manager = DialogManager::new_with_engine(EngineType::Fantoccini);
356        assert_eq!(manager.engine(), EngineType::Fantoccini);
357    }
358
359    #[test]
360    fn dialog_manager_debug() {
361        let manager = DialogManager::new();
362        let debug_str = format!("{:?}", manager);
363        assert!(debug_str.contains("DialogManager"));
364    }
365}