Skip to main content

jugar_probar/
dialog.rs

1//! Dialog Handling for E2E Testing (Feature G.8)
2//!
3//! Provides support for handling browser dialogs (alert, confirm, prompt, beforeunload).
4
5use serde::{Deserialize, Serialize};
6use std::sync::{Arc, Mutex};
7
8/// Type of browser dialog
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum DialogType {
11    /// Alert dialog (OK button only)
12    Alert,
13    /// Confirm dialog (OK/Cancel buttons)
14    Confirm,
15    /// Prompt dialog (text input + OK/Cancel)
16    Prompt,
17    /// Before unload dialog (Leave/Stay buttons)
18    BeforeUnload,
19}
20
21impl std::fmt::Display for DialogType {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            Self::Alert => write!(f, "alert"),
25            Self::Confirm => write!(f, "confirm"),
26            Self::Prompt => write!(f, "prompt"),
27            Self::BeforeUnload => write!(f, "beforeunload"),
28        }
29    }
30}
31
32/// Action taken on a dialog
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub enum DialogAction {
35    /// Dialog was accepted (OK/Yes/Leave)
36    Accept,
37    /// Dialog was accepted with input text (for prompts)
38    AcceptWith(String),
39    /// Dialog was dismissed (Cancel/No/Stay)
40    Dismiss,
41    /// Dialog is pending (not yet handled)
42    Pending,
43}
44
45/// Represents a browser dialog
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Dialog {
48    /// Type of dialog
49    dialog_type: DialogType,
50    /// Message displayed in the dialog
51    message: String,
52    /// Default value (for prompt dialogs)
53    default_value: Option<String>,
54    /// Action taken
55    action: DialogAction,
56}
57
58impl Dialog {
59    /// Create a new dialog
60    #[must_use]
61    pub fn new(dialog_type: DialogType, message: impl Into<String>) -> Self {
62        Self {
63            dialog_type,
64            message: message.into(),
65            default_value: None,
66            action: DialogAction::Pending,
67        }
68    }
69
70    /// Create an alert dialog
71    #[must_use]
72    pub fn alert(message: impl Into<String>) -> Self {
73        Self::new(DialogType::Alert, message)
74    }
75
76    /// Create a confirm dialog
77    #[must_use]
78    pub fn confirm(message: impl Into<String>) -> Self {
79        Self::new(DialogType::Confirm, message)
80    }
81
82    /// Create a prompt dialog
83    #[must_use]
84    pub fn prompt(message: impl Into<String>, default: Option<String>) -> Self {
85        let mut dialog = Self::new(DialogType::Prompt, message);
86        dialog.default_value = default;
87        dialog
88    }
89
90    /// Create a beforeunload dialog
91    #[must_use]
92    pub fn before_unload(message: impl Into<String>) -> Self {
93        Self::new(DialogType::BeforeUnload, message)
94    }
95
96    /// Get dialog type
97    #[must_use]
98    pub fn dialog_type(&self) -> DialogType {
99        self.dialog_type
100    }
101
102    /// Get dialog message
103    #[must_use]
104    pub fn message(&self) -> &str {
105        &self.message
106    }
107
108    /// Get default value (for prompts)
109    #[must_use]
110    pub fn default_value(&self) -> Option<&str> {
111        self.default_value.as_deref()
112    }
113
114    /// Get action taken
115    #[must_use]
116    pub fn action(&self) -> &DialogAction {
117        &self.action
118    }
119
120    /// Check if dialog was handled
121    #[must_use]
122    pub fn is_handled(&self) -> bool {
123        !matches!(self.action, DialogAction::Pending)
124    }
125
126    /// Accept the dialog
127    pub fn accept(&mut self) {
128        self.action = DialogAction::Accept;
129    }
130
131    /// Accept the dialog with input text (for prompts)
132    pub fn accept_with(&mut self, text: impl Into<String>) {
133        self.action = DialogAction::AcceptWith(text.into());
134    }
135
136    /// Dismiss the dialog
137    pub fn dismiss(&mut self) {
138        self.action = DialogAction::Dismiss;
139    }
140}
141
142/// Handler function type for dialogs
143pub type DialogHandlerFn = Box<dyn Fn(&mut Dialog) + Send + Sync>;
144
145/// Configuration for automatic dialog handling
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147pub enum AutoDialogBehavior {
148    /// Accept all dialogs automatically
149    AcceptAll,
150    /// Dismiss all dialogs automatically
151    DismissAll,
152    /// Accept with empty string (for prompts)
153    AcceptEmpty,
154    /// Use default value (for prompts)
155    UseDefault,
156    /// Do nothing (let tests handle manually)
157    Manual,
158}
159
160impl Default for AutoDialogBehavior {
161    fn default() -> Self {
162        Self::Manual
163    }
164}
165
166/// Dialog handler for managing browser dialogs
167#[derive(Clone)]
168pub struct DialogHandler {
169    /// Queue of dialogs encountered
170    dialogs: Arc<Mutex<Vec<Dialog>>>,
171    /// Custom handler function
172    handler: Arc<Mutex<Option<DialogHandlerFn>>>,
173    /// Auto behavior when no custom handler
174    auto_behavior: Arc<Mutex<AutoDialogBehavior>>,
175}
176
177impl DialogHandler {
178    /// Create a new dialog handler
179    #[must_use]
180    pub fn new() -> Self {
181        Self {
182            dialogs: Arc::new(Mutex::new(Vec::new())),
183            handler: Arc::new(Mutex::new(None)),
184            auto_behavior: Arc::new(Mutex::new(AutoDialogBehavior::default())),
185        }
186    }
187
188    /// Set custom handler function
189    pub fn on_dialog<F>(&self, handler: F)
190    where
191        F: Fn(&mut Dialog) + Send + Sync + 'static,
192    {
193        if let Ok(mut h) = self.handler.lock() {
194            *h = Some(Box::new(handler));
195        }
196    }
197
198    /// Set automatic behavior
199    pub fn set_auto_behavior(&self, behavior: AutoDialogBehavior) {
200        if let Ok(mut b) = self.auto_behavior.lock() {
201            *b = behavior;
202        }
203    }
204
205    /// Handle an incoming dialog
206    pub fn handle(&self, mut dialog: Dialog) -> Dialog {
207        // Try custom handler first
208        if let Ok(handler) = self.handler.lock() {
209            if let Some(ref h) = *handler {
210                h(&mut dialog);
211                if dialog.is_handled() {
212                    if let Ok(mut dialogs) = self.dialogs.lock() {
213                        dialogs.push(dialog.clone());
214                    }
215                    return dialog;
216                }
217            }
218        }
219
220        // Fall back to auto behavior
221        let behavior = self.auto_behavior.lock().map(|b| *b).unwrap_or_default();
222        match behavior {
223            AutoDialogBehavior::AcceptAll => dialog.accept(),
224            AutoDialogBehavior::DismissAll => dialog.dismiss(),
225            AutoDialogBehavior::AcceptEmpty => dialog.accept_with(""),
226            AutoDialogBehavior::UseDefault => {
227                if let Some(default) = dialog.default_value.clone() {
228                    dialog.accept_with(default);
229                } else {
230                    dialog.accept();
231                }
232            }
233            AutoDialogBehavior::Manual => {
234                // Leave as pending
235            }
236        }
237
238        if let Ok(mut dialogs) = self.dialogs.lock() {
239            dialogs.push(dialog.clone());
240        }
241        dialog
242    }
243
244    /// Get all dialogs encountered
245    #[must_use]
246    pub fn dialogs(&self) -> Vec<Dialog> {
247        self.dialogs.lock().map(|d| d.clone()).unwrap_or_default()
248    }
249
250    /// Get count of dialogs
251    #[must_use]
252    pub fn dialog_count(&self) -> usize {
253        self.dialogs.lock().map(|d| d.len()).unwrap_or(0)
254    }
255
256    /// Clear dialog history
257    pub fn clear(&self) {
258        if let Ok(mut d) = self.dialogs.lock() {
259            d.clear();
260        }
261    }
262
263    /// Check if any dialogs are pending
264    #[must_use]
265    pub fn has_pending(&self) -> bool {
266        self.dialogs
267            .lock()
268            .map(|d| d.iter().any(|dialog| !dialog.is_handled()))
269            .unwrap_or(false)
270    }
271
272    /// Get last dialog
273    #[must_use]
274    pub fn last_dialog(&self) -> Option<Dialog> {
275        self.dialogs.lock().ok().and_then(|d| d.last().cloned())
276    }
277
278    /// Wait for a dialog (mock implementation for sync tests)
279    #[must_use]
280    pub fn expect_dialog(&self, dialog_type: DialogType) -> DialogExpectation {
281        DialogExpectation {
282            expected_type: dialog_type,
283            handler: self.clone(),
284        }
285    }
286}
287
288impl Default for DialogHandler {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294impl std::fmt::Debug for DialogHandler {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        let auto_behavior = self.auto_behavior.lock().map(|b| *b).unwrap_or_default();
297        f.debug_struct("DialogHandler")
298            .field("dialog_count", &self.dialog_count())
299            .field("auto_behavior", &auto_behavior)
300            .finish()
301    }
302}
303
304/// Expectation for a specific dialog type
305#[derive(Debug)]
306pub struct DialogExpectation {
307    expected_type: DialogType,
308    handler: DialogHandler,
309}
310
311impl DialogExpectation {
312    /// Verify the expected dialog was encountered
313    #[must_use]
314    pub fn verify(&self) -> bool {
315        self.handler
316            .dialogs()
317            .iter()
318            .any(|d| d.dialog_type() == self.expected_type)
319    }
320
321    /// Get the dialog if it matches
322    #[must_use]
323    pub fn dialog(&self) -> Option<Dialog> {
324        self.handler
325            .dialogs()
326            .into_iter()
327            .find(|d| d.dialog_type() == self.expected_type)
328    }
329}
330
331/// Builder for dialog handler
332#[derive(Debug, Clone)]
333pub struct DialogHandlerBuilder {
334    auto_behavior: AutoDialogBehavior,
335}
336
337impl DialogHandlerBuilder {
338    /// Create a new builder
339    #[must_use]
340    pub fn new() -> Self {
341        Self {
342            auto_behavior: AutoDialogBehavior::default(),
343        }
344    }
345
346    /// Accept all dialogs automatically
347    #[must_use]
348    pub fn accept_all(mut self) -> Self {
349        self.auto_behavior = AutoDialogBehavior::AcceptAll;
350        self
351    }
352
353    /// Dismiss all dialogs automatically
354    #[must_use]
355    pub fn dismiss_all(mut self) -> Self {
356        self.auto_behavior = AutoDialogBehavior::DismissAll;
357        self
358    }
359
360    /// Use default values for prompts
361    #[must_use]
362    pub fn use_defaults(mut self) -> Self {
363        self.auto_behavior = AutoDialogBehavior::UseDefault;
364        self
365    }
366
367    /// Build the handler
368    #[must_use]
369    pub fn build(self) -> DialogHandler {
370        let handler = DialogHandler::new();
371        handler.set_auto_behavior(self.auto_behavior);
372        handler
373    }
374}
375
376impl Default for DialogHandlerBuilder {
377    fn default() -> Self {
378        Self::new()
379    }
380}
381
382#[cfg(test)]
383#[allow(clippy::unwrap_used, clippy::expect_used)]
384mod tests {
385    use super::*;
386
387    // =========================================================================
388    // H₀-DIALOG-01: Dialog creation
389    // =========================================================================
390
391    #[test]
392    fn h0_dialog_01_new() {
393        let dialog = Dialog::new(DialogType::Alert, "Hello");
394        assert_eq!(dialog.dialog_type(), DialogType::Alert);
395        assert_eq!(dialog.message(), "Hello");
396        assert!(!dialog.is_handled());
397    }
398
399    #[test]
400    fn h0_dialog_02_alert() {
401        let dialog = Dialog::alert("Test alert");
402        assert_eq!(dialog.dialog_type(), DialogType::Alert);
403    }
404
405    #[test]
406    fn h0_dialog_03_confirm() {
407        let dialog = Dialog::confirm("Are you sure?");
408        assert_eq!(dialog.dialog_type(), DialogType::Confirm);
409    }
410
411    #[test]
412    fn h0_dialog_04_prompt() {
413        let dialog = Dialog::prompt("Enter name:", Some("default".to_string()));
414        assert_eq!(dialog.dialog_type(), DialogType::Prompt);
415        assert_eq!(dialog.default_value(), Some("default"));
416    }
417
418    #[test]
419    fn h0_dialog_05_before_unload() {
420        let dialog = Dialog::before_unload("Leave page?");
421        assert_eq!(dialog.dialog_type(), DialogType::BeforeUnload);
422    }
423
424    // =========================================================================
425    // H₀-DIALOG-06: Dialog actions
426    // =========================================================================
427
428    #[test]
429    fn h0_dialog_06_accept() {
430        let mut dialog = Dialog::alert("Test");
431        dialog.accept();
432        assert!(dialog.is_handled());
433        assert_eq!(dialog.action(), &DialogAction::Accept);
434    }
435
436    #[test]
437    fn h0_dialog_07_accept_with() {
438        let mut dialog = Dialog::prompt("Name?", None);
439        dialog.accept_with("John");
440        assert!(dialog.is_handled());
441        assert_eq!(
442            dialog.action(),
443            &DialogAction::AcceptWith("John".to_string())
444        );
445    }
446
447    #[test]
448    fn h0_dialog_08_dismiss() {
449        let mut dialog = Dialog::confirm("Continue?");
450        dialog.dismiss();
451        assert!(dialog.is_handled());
452        assert_eq!(dialog.action(), &DialogAction::Dismiss);
453    }
454
455    // =========================================================================
456    // H₀-DIALOG-09: DialogType display
457    // =========================================================================
458
459    #[test]
460    fn h0_dialog_09_type_display() {
461        assert_eq!(format!("{}", DialogType::Alert), "alert");
462        assert_eq!(format!("{}", DialogType::Confirm), "confirm");
463        assert_eq!(format!("{}", DialogType::Prompt), "prompt");
464        assert_eq!(format!("{}", DialogType::BeforeUnload), "beforeunload");
465    }
466
467    // =========================================================================
468    // H₀-DIALOG-10: DialogHandler creation
469    // =========================================================================
470
471    #[test]
472    fn h0_dialog_10_handler_new() {
473        let handler = DialogHandler::new();
474        assert_eq!(handler.dialog_count(), 0);
475        assert!(!handler.has_pending());
476    }
477
478    #[test]
479    fn h0_dialog_11_handler_default() {
480        let handler = DialogHandler::default();
481        assert_eq!(handler.dialog_count(), 0);
482    }
483
484    // =========================================================================
485    // H₀-DIALOG-12: Auto behavior
486    // =========================================================================
487
488    #[test]
489    fn h0_dialog_12_auto_accept_all() {
490        let handler = DialogHandler::new();
491        handler.set_auto_behavior(AutoDialogBehavior::AcceptAll);
492
493        let dialog = handler.handle(Dialog::confirm("Test?"));
494        assert_eq!(dialog.action(), &DialogAction::Accept);
495    }
496
497    #[test]
498    fn h0_dialog_13_auto_dismiss_all() {
499        let handler = DialogHandler::new();
500        handler.set_auto_behavior(AutoDialogBehavior::DismissAll);
501
502        let dialog = handler.handle(Dialog::confirm("Test?"));
503        assert_eq!(dialog.action(), &DialogAction::Dismiss);
504    }
505
506    #[test]
507    fn h0_dialog_14_auto_accept_empty() {
508        let handler = DialogHandler::new();
509        handler.set_auto_behavior(AutoDialogBehavior::AcceptEmpty);
510
511        let dialog = handler.handle(Dialog::prompt("Name?", None));
512        assert_eq!(dialog.action(), &DialogAction::AcceptWith(String::new()));
513    }
514
515    #[test]
516    fn h0_dialog_15_auto_use_default() {
517        let handler = DialogHandler::new();
518        handler.set_auto_behavior(AutoDialogBehavior::UseDefault);
519
520        let dialog = handler.handle(Dialog::prompt("Name?", Some("John".to_string())));
521        assert_eq!(
522            dialog.action(),
523            &DialogAction::AcceptWith("John".to_string())
524        );
525    }
526
527    #[test]
528    fn h0_dialog_16_auto_manual() {
529        let handler = DialogHandler::new();
530        handler.set_auto_behavior(AutoDialogBehavior::Manual);
531
532        let dialog = handler.handle(Dialog::alert("Test"));
533        assert_eq!(dialog.action(), &DialogAction::Pending);
534    }
535
536    // =========================================================================
537    // H₀-DIALOG-17: Custom handler
538    // =========================================================================
539
540    #[test]
541    fn h0_dialog_17_custom_handler() {
542        let handler = DialogHandler::new();
543        handler.on_dialog(|dialog| {
544            if dialog.dialog_type() == DialogType::Confirm {
545                dialog.accept();
546            } else {
547                dialog.dismiss();
548            }
549        });
550
551        let confirm = handler.handle(Dialog::confirm("Continue?"));
552        assert_eq!(confirm.action(), &DialogAction::Accept);
553
554        let alert = handler.handle(Dialog::alert("Info"));
555        assert_eq!(alert.action(), &DialogAction::Dismiss);
556    }
557
558    // =========================================================================
559    // H₀-DIALOG-18: Dialog history
560    // =========================================================================
561
562    #[test]
563    fn h0_dialog_18_dialogs() {
564        let handler = DialogHandler::new();
565        handler.set_auto_behavior(AutoDialogBehavior::AcceptAll);
566
567        handler.handle(Dialog::alert("First"));
568        handler.handle(Dialog::alert("Second"));
569
570        let dialogs = handler.dialogs();
571        assert_eq!(dialogs.len(), 2);
572        assert_eq!(dialogs[0].message(), "First");
573        assert_eq!(dialogs[1].message(), "Second");
574    }
575
576    #[test]
577    fn h0_dialog_19_last_dialog() {
578        let handler = DialogHandler::new();
579        handler.set_auto_behavior(AutoDialogBehavior::AcceptAll);
580
581        handler.handle(Dialog::alert("First"));
582        handler.handle(Dialog::alert("Last"));
583
584        let last = handler.last_dialog().unwrap();
585        assert_eq!(last.message(), "Last");
586    }
587
588    #[test]
589    fn h0_dialog_20_clear() {
590        let handler = DialogHandler::new();
591        handler.set_auto_behavior(AutoDialogBehavior::AcceptAll);
592        handler.handle(Dialog::alert("Test"));
593
594        handler.clear();
595
596        assert_eq!(handler.dialog_count(), 0);
597    }
598
599    // =========================================================================
600    // H₀-DIALOG-21: Has pending
601    // =========================================================================
602
603    #[test]
604    fn h0_dialog_21_has_pending_false() {
605        let handler = DialogHandler::new();
606        handler.set_auto_behavior(AutoDialogBehavior::AcceptAll);
607        handler.handle(Dialog::alert("Test"));
608
609        assert!(!handler.has_pending());
610    }
611
612    #[test]
613    fn h0_dialog_22_has_pending_true() {
614        let handler = DialogHandler::new();
615        handler.set_auto_behavior(AutoDialogBehavior::Manual);
616        handler.handle(Dialog::alert("Test"));
617
618        assert!(handler.has_pending());
619    }
620
621    // =========================================================================
622    // H₀-DIALOG-23: Expect dialog
623    // =========================================================================
624
625    #[test]
626    fn h0_dialog_23_expect_dialog_verify() {
627        let handler = DialogHandler::new();
628        handler.set_auto_behavior(AutoDialogBehavior::AcceptAll);
629        handler.handle(Dialog::confirm("Sure?"));
630
631        let expectation = handler.expect_dialog(DialogType::Confirm);
632        assert!(expectation.verify());
633    }
634
635    #[test]
636    fn h0_dialog_24_expect_dialog_not_found() {
637        let handler = DialogHandler::new();
638        handler.set_auto_behavior(AutoDialogBehavior::AcceptAll);
639        handler.handle(Dialog::alert("Info"));
640
641        let expectation = handler.expect_dialog(DialogType::Confirm);
642        assert!(!expectation.verify());
643    }
644
645    #[test]
646    fn h0_dialog_25_expect_dialog_get() {
647        let handler = DialogHandler::new();
648        handler.set_auto_behavior(AutoDialogBehavior::AcceptAll);
649        handler.handle(Dialog::prompt("Name?", None));
650
651        let expectation = handler.expect_dialog(DialogType::Prompt);
652        let dialog = expectation.dialog().unwrap();
653        assert_eq!(dialog.message(), "Name?");
654    }
655
656    // =========================================================================
657    // H₀-DIALOG-26: Builder
658    // =========================================================================
659
660    #[test]
661    fn h0_dialog_26_builder_accept_all() {
662        let handler = DialogHandlerBuilder::new().accept_all().build();
663
664        let dialog = handler.handle(Dialog::alert("Test"));
665        assert_eq!(dialog.action(), &DialogAction::Accept);
666    }
667
668    #[test]
669    fn h0_dialog_27_builder_dismiss_all() {
670        let handler = DialogHandlerBuilder::new().dismiss_all().build();
671
672        let dialog = handler.handle(Dialog::confirm("Sure?"));
673        assert_eq!(dialog.action(), &DialogAction::Dismiss);
674    }
675
676    #[test]
677    fn h0_dialog_28_builder_use_defaults() {
678        let handler = DialogHandlerBuilder::new().use_defaults().build();
679
680        let dialog = handler.handle(Dialog::prompt("Name?", Some("Bob".to_string())));
681        assert_eq!(
682            dialog.action(),
683            &DialogAction::AcceptWith("Bob".to_string())
684        );
685    }
686
687    // =========================================================================
688    // H₀-DIALOG-29: Debug
689    // =========================================================================
690
691    #[test]
692    fn h0_dialog_29_handler_debug() {
693        let handler = DialogHandler::new();
694        let debug = format!("{handler:?}");
695        assert!(debug.contains("DialogHandler"));
696        assert!(debug.contains("dialog_count"));
697    }
698
699    // =========================================================================
700    // H₀-DIALOG-30: Clone
701    // =========================================================================
702
703    #[test]
704    fn h0_dialog_30_dialog_clone() {
705        let dialog = Dialog::alert("Test");
706        let cloned = dialog;
707        assert_eq!(cloned.message(), "Test");
708    }
709
710    #[test]
711    fn h0_dialog_31_handler_clone() {
712        let handler = DialogHandler::new();
713        handler.set_auto_behavior(AutoDialogBehavior::AcceptAll);
714        handler.handle(Dialog::alert("Test"));
715
716        let cloned = handler;
717        assert_eq!(cloned.dialog_count(), 1);
718    }
719}