oxidize_pdf/forms/
field_actions.rs

1//! Field action handling for interactive forms according to ISO 32000-1 Section 12.6.3
2//!
3//! This module provides support for field actions including:
4//! - Focus events (Fo) - when a field receives focus
5//! - Blur events (Bl) - when a field loses focus  
6//! - Format events (F) - before displaying value
7//! - Keystroke events (K) - during text input
8//! - Calculate events (C) - after field value changes
9//! - Validate events (V) - before committing value
10
11use crate::error::PdfError;
12use crate::forms::calculations::FieldValue;
13use crate::objects::{Dictionary, Object};
14use chrono::{DateTime, Utc};
15use std::collections::HashMap;
16use std::fmt;
17
18/// Field action system for handling interactive events
19#[derive(Debug, Clone, Default)]
20pub struct FieldActionSystem {
21    /// Registered actions by field
22    actions: HashMap<String, FieldActions>,
23    /// Action event history
24    event_history: Vec<ActionEvent>,
25    /// Current focused field
26    focused_field: Option<String>,
27    /// Action handlers
28    handlers: ActionHandlers,
29    /// Settings
30    settings: ActionSettings,
31}
32
33/// Actions for a specific field
34#[derive(Debug, Clone, Default)]
35pub struct FieldActions {
36    /// Focus action (field receives focus)
37    pub on_focus: Option<FieldAction>,
38    /// Blur action (field loses focus)
39    pub on_blur: Option<FieldAction>,
40    /// Format action (before display)
41    pub on_format: Option<FieldAction>,
42    /// Keystroke action (during input)
43    pub on_keystroke: Option<FieldAction>,
44    /// Calculate action (after value change)
45    pub on_calculate: Option<FieldAction>,
46    /// Validate action (before commit)
47    pub on_validate: Option<FieldAction>,
48    /// Mouse enter action
49    pub on_mouse_enter: Option<FieldAction>,
50    /// Mouse exit action
51    pub on_mouse_exit: Option<FieldAction>,
52    /// Mouse down action
53    pub on_mouse_down: Option<FieldAction>,
54    /// Mouse up action
55    pub on_mouse_up: Option<FieldAction>,
56}
57
58/// Field action types
59#[derive(Debug, Clone)]
60pub enum FieldAction {
61    /// JavaScript action
62    JavaScript { script: String, async_exec: bool },
63    /// Format action
64    Format { format_type: FormatActionType },
65    /// Validate action
66    Validate { validation_type: ValidateActionType },
67    /// Calculate action
68    Calculate { expression: String },
69    /// Submit form action
70    SubmitForm {
71        url: String,
72        fields: Vec<String>,
73        include_empty: bool,
74    },
75    /// Reset form action
76    ResetForm { fields: Vec<String>, exclude: bool },
77    /// Import data action
78    ImportData { file_path: String },
79    /// Set field action
80    SetField {
81        target_field: String,
82        value: FieldValue,
83    },
84    /// Show/Hide field action
85    ShowHide { fields: Vec<String>, show: bool },
86    /// Play sound action
87    PlaySound { sound_name: String, volume: f32 },
88    /// Custom action
89    Custom {
90        action_type: String,
91        parameters: HashMap<String, String>,
92    },
93}
94
95/// Format action types
96#[derive(Debug, Clone)]
97pub enum FormatActionType {
98    /// Number format
99    Number {
100        decimals: usize,
101        currency: Option<String>,
102    },
103    /// Percentage format
104    Percent { decimals: usize },
105    /// Date format
106    Date { format: String },
107    /// Time format  
108    Time { format: String },
109    /// Special format (SSN, Phone, Zip)
110    Special { format: SpecialFormatType },
111    /// Custom format script
112    Custom { script: String },
113}
114
115/// Special format types
116#[derive(Debug, Clone, Copy)]
117pub enum SpecialFormatType {
118    ZipCode,
119    ZipPlus4,
120    Phone,
121    SSN,
122}
123
124/// Validate action types
125#[derive(Debug, Clone)]
126pub enum ValidateActionType {
127    /// Range validation
128    Range { min: Option<f64>, max: Option<f64> },
129    /// Custom validation script
130    Custom { script: String },
131}
132
133/// Action event record
134#[derive(Debug, Clone)]
135pub struct ActionEvent {
136    /// Timestamp
137    pub timestamp: DateTime<Utc>,
138    /// Field name
139    pub field_name: String,
140    /// Event type
141    pub event_type: ActionEventType,
142    /// Action executed
143    pub action: Option<FieldAction>,
144    /// Result
145    pub result: ActionResult,
146    /// Additional data
147    pub data: HashMap<String, String>,
148}
149
150/// Action event types
151#[derive(Debug, Clone, PartialEq)]
152pub enum ActionEventType {
153    Focus,
154    Blur,
155    Format,
156    Keystroke,
157    Calculate,
158    Validate,
159    MouseEnter,
160    MouseExit,
161    MouseDown,
162    MouseUp,
163}
164
165/// Action execution result
166#[derive(Debug, Clone)]
167pub enum ActionResult {
168    Success,
169    Failed(String),
170    Cancelled,
171    Modified(FieldValue),
172}
173
174/// Type alias for JavaScript executor function
175pub type JsExecutor = fn(&str) -> Result<String, String>;
176
177/// Type alias for form submitter function
178pub type FormSubmitter = fn(&str, &[String]) -> Result<(), String>;
179
180/// Type alias for sound player function
181pub type SoundPlayer = fn(&str, f32) -> Result<(), String>;
182
183/// Type alias for custom handler function
184pub type CustomHandler = fn(&str, &HashMap<String, String>) -> Result<(), String>;
185
186/// Action handlers
187#[derive(Clone, Default)]
188pub struct ActionHandlers {
189    /// JavaScript executor
190    pub js_executor: Option<JsExecutor>,
191    /// Form submitter
192    pub form_submitter: Option<FormSubmitter>,
193    /// Sound player
194    pub sound_player: Option<SoundPlayer>,
195    /// Custom handler
196    pub custom_handler: Option<CustomHandler>,
197}
198
199impl fmt::Debug for ActionHandlers {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.debug_struct("ActionHandlers")
202            .field("js_executor", &self.js_executor.is_some())
203            .field("form_submitter", &self.form_submitter.is_some())
204            .field("sound_player", &self.sound_player.is_some())
205            .field("custom_handler", &self.custom_handler.is_some())
206            .finish()
207    }
208}
209
210/// Action system settings
211#[derive(Debug, Clone)]
212pub struct ActionSettings {
213    /// Enable JavaScript execution
214    pub enable_javascript: bool,
215    /// Enable form submission
216    pub enable_form_submit: bool,
217    /// Enable sound actions
218    pub enable_sound: bool,
219    /// Log all events
220    pub log_events: bool,
221    /// Maximum event history
222    pub max_event_history: usize,
223}
224
225impl Default for ActionSettings {
226    fn default() -> Self {
227        Self {
228            enable_javascript: true,
229            enable_form_submit: false,
230            enable_sound: true,
231            log_events: true,
232            max_event_history: 1000,
233        }
234    }
235}
236
237impl FieldActionSystem {
238    /// Create a new field action system
239    pub fn new() -> Self {
240        Self::default()
241    }
242
243    /// Create with custom settings
244    pub fn with_settings(settings: ActionSettings) -> Self {
245        Self {
246            settings,
247            ..Self::default()
248        }
249    }
250
251    /// Register field actions
252    pub fn register_field_actions(&mut self, field_name: impl Into<String>, actions: FieldActions) {
253        self.actions.insert(field_name.into(), actions);
254    }
255
256    /// Handle focus event
257    pub fn handle_focus(&mut self, field_name: impl Into<String>) -> Result<(), PdfError> {
258        let field_name = field_name.into();
259
260        // Handle blur on previously focused field
261        if let Some(prev_field) = self.focused_field.clone() {
262            if prev_field != field_name {
263                self.handle_blur(prev_field)?;
264            }
265        }
266
267        // Update focused field
268        self.focused_field = Some(field_name.clone());
269
270        // Execute focus action if registered
271        if let Some(action) = self
272            .actions
273            .get(&field_name)
274            .and_then(|a| a.on_focus.clone())
275        {
276            self.execute_action(&field_name, ActionEventType::Focus, &action)?;
277        }
278
279        Ok(())
280    }
281
282    /// Handle blur event
283    pub fn handle_blur(&mut self, field_name: impl Into<String>) -> Result<(), PdfError> {
284        let field_name = field_name.into();
285
286        // Only handle if field was focused
287        if self.focused_field.as_ref() == Some(&field_name) {
288            self.focused_field = None;
289
290            // Execute blur action if registered
291            if let Some(action) = self
292                .actions
293                .get(&field_name)
294                .and_then(|a| a.on_blur.clone())
295            {
296                self.execute_action(&field_name, ActionEventType::Blur, &action)?;
297            }
298        }
299
300        Ok(())
301    }
302
303    /// Handle format event
304    pub fn handle_format(
305        &mut self,
306        field_name: impl Into<String>,
307        value: &mut FieldValue,
308    ) -> Result<(), PdfError> {
309        let field_name = field_name.into();
310
311        if let Some(action) = self
312            .actions
313            .get(&field_name)
314            .and_then(|a| a.on_format.clone())
315        {
316            let result = self.execute_action(&field_name, ActionEventType::Format, &action)?;
317
318            // Apply format result if modified
319            if let ActionResult::Modified(new_value) = result {
320                *value = new_value;
321            }
322        }
323
324        Ok(())
325    }
326
327    /// Handle keystroke event
328    pub fn handle_keystroke(
329        &mut self,
330        field_name: impl Into<String>,
331        _key: char,
332        _current_value: &str,
333    ) -> Result<bool, PdfError> {
334        let field_name = field_name.into();
335
336        if let Some(action) = self
337            .actions
338            .get(&field_name)
339            .and_then(|a| a.on_keystroke.clone())
340        {
341            let result = self.execute_action(&field_name, ActionEventType::Keystroke, &action)?;
342
343            // Check if keystroke should be accepted
344            return match result {
345                ActionResult::Success => Ok(true),
346                ActionResult::Cancelled => Ok(false),
347                _ => Ok(true),
348            };
349        }
350
351        Ok(true)
352    }
353
354    /// Handle validate event
355    pub fn handle_validate(
356        &mut self,
357        field_name: impl Into<String>,
358        _value: &FieldValue,
359    ) -> Result<bool, PdfError> {
360        let field_name = field_name.into();
361
362        if let Some(action) = self
363            .actions
364            .get(&field_name)
365            .and_then(|a| a.on_validate.clone())
366        {
367            let result = self.execute_action(&field_name, ActionEventType::Validate, &action)?;
368
369            // Check validation result
370            return match result {
371                ActionResult::Success => Ok(true),
372                ActionResult::Failed(_) => Ok(false),
373                _ => Ok(true),
374            };
375        }
376
377        Ok(true)
378    }
379
380    /// Handle calculate event
381    pub fn handle_calculate(
382        &mut self,
383        field_name: impl Into<String>,
384        value: &mut FieldValue,
385    ) -> Result<(), PdfError> {
386        let field_name = field_name.into();
387
388        if let Some(action) = self
389            .actions
390            .get(&field_name)
391            .and_then(|a| a.on_calculate.clone())
392        {
393            let result = self.execute_action(&field_name, ActionEventType::Calculate, &action)?;
394
395            // Apply calculated value if modified
396            if let ActionResult::Modified(new_value) = result {
397                *value = new_value;
398            }
399        }
400
401        Ok(())
402    }
403
404    /// Execute an action
405    fn execute_action(
406        &mut self,
407        field_name: &str,
408        event_type: ActionEventType,
409        action: &FieldAction,
410    ) -> Result<ActionResult, PdfError> {
411        let result = match action {
412            FieldAction::JavaScript { script, async_exec } => {
413                self.execute_javascript(script, *async_exec)
414            }
415            FieldAction::Format { format_type } => self.execute_format(format_type),
416            FieldAction::Validate { validation_type } => self.execute_validate(validation_type),
417            FieldAction::Calculate { expression } => self.execute_calculate(expression),
418            FieldAction::ShowHide { fields, show } => self.execute_show_hide(fields, *show),
419            FieldAction::SetField {
420                target_field,
421                value,
422            } => self.execute_set_field(target_field, value),
423            _ => Ok(ActionResult::Success),
424        };
425
426        // Log event if enabled
427        if self.settings.log_events {
428            let result_for_log = result
429                .as_ref()
430                .map(|r| r.clone())
431                .unwrap_or_else(|e| ActionResult::Failed(e.to_string()));
432            self.log_event(field_name, event_type, Some(action.clone()), result_for_log);
433        }
434
435        result
436    }
437
438    /// Execute JavaScript action
439    fn execute_javascript(
440        &self,
441        script: &str,
442        _async_exec: bool,
443    ) -> Result<ActionResult, PdfError> {
444        if !self.settings.enable_javascript {
445            return Ok(ActionResult::Cancelled);
446        }
447
448        if let Some(executor) = self.handlers.js_executor {
449            match executor(script) {
450                Ok(_result) => Ok(ActionResult::Success),
451                Err(e) => Ok(ActionResult::Failed(e)),
452            }
453        } else {
454            // Default implementation - just succeed
455            Ok(ActionResult::Success)
456        }
457    }
458
459    /// Execute format action
460    fn execute_format(&self, _format_type: &FormatActionType) -> Result<ActionResult, PdfError> {
461        // Format implementation would go here
462        Ok(ActionResult::Success)
463    }
464
465    /// Execute validate action
466    fn execute_validate(
467        &self,
468        validation_type: &ValidateActionType,
469    ) -> Result<ActionResult, PdfError> {
470        match validation_type {
471            ValidateActionType::Range { min: _, max: _ } => {
472                // Range validation would go here
473                Ok(ActionResult::Success)
474            }
475            ValidateActionType::Custom { script } => self.execute_javascript(script, false),
476        }
477    }
478
479    /// Execute calculate action
480    fn execute_calculate(&self, _expression: &str) -> Result<ActionResult, PdfError> {
481        // Calculation would go here
482        Ok(ActionResult::Success)
483    }
484
485    /// Execute show/hide action
486    fn execute_show_hide(&self, _fields: &[String], _show: bool) -> Result<ActionResult, PdfError> {
487        // Show/hide implementation would go here
488        Ok(ActionResult::Success)
489    }
490
491    /// Execute set field action
492    fn execute_set_field(
493        &self,
494        _target_field: &str,
495        value: &FieldValue,
496    ) -> Result<ActionResult, PdfError> {
497        // Set field implementation would go here
498        Ok(ActionResult::Modified(value.clone()))
499    }
500
501    /// Log an event
502    fn log_event(
503        &mut self,
504        field_name: &str,
505        event_type: ActionEventType,
506        action: Option<FieldAction>,
507        result: ActionResult,
508    ) {
509        let event = ActionEvent {
510            timestamp: Utc::now(),
511            field_name: field_name.to_string(),
512            event_type,
513            action,
514            result,
515            data: HashMap::new(),
516        };
517
518        self.event_history.push(event);
519
520        // Trim history if needed
521        if self.event_history.len() > self.settings.max_event_history {
522            self.event_history.remove(0);
523        }
524    }
525
526    /// Get current focused field
527    pub fn get_focused_field(&self) -> Option<&String> {
528        self.focused_field.as_ref()
529    }
530
531    /// Get event history
532    pub fn get_event_history(&self) -> &[ActionEvent] {
533        &self.event_history
534    }
535
536    /// Clear event history
537    pub fn clear_event_history(&mut self) {
538        self.event_history.clear();
539    }
540
541    /// Set JavaScript executor
542    pub fn set_js_executor(&mut self, executor: fn(&str) -> Result<String, String>) {
543        self.handlers.js_executor = Some(executor);
544    }
545
546    /// Export actions to PDF dictionary
547    pub fn to_pdf_dict(&self, field_name: &str) -> Dictionary {
548        let mut dict = Dictionary::new();
549
550        if let Some(actions) = self.actions.get(field_name) {
551            // Add additional actions (AA) dictionary
552            let mut aa_dict = Dictionary::new();
553
554            if let Some(action) = &actions.on_focus {
555                aa_dict.set("Fo", self.action_to_object(action));
556            }
557            if let Some(action) = &actions.on_blur {
558                aa_dict.set("Bl", self.action_to_object(action));
559            }
560            if let Some(action) = &actions.on_format {
561                aa_dict.set("F", self.action_to_object(action));
562            }
563            if let Some(action) = &actions.on_keystroke {
564                aa_dict.set("K", self.action_to_object(action));
565            }
566            if let Some(action) = &actions.on_calculate {
567                aa_dict.set("C", self.action_to_object(action));
568            }
569            if let Some(action) = &actions.on_validate {
570                aa_dict.set("V", self.action_to_object(action));
571            }
572
573            if !aa_dict.is_empty() {
574                dict.set("AA", Object::Dictionary(aa_dict));
575            }
576        }
577
578        dict
579    }
580
581    /// Convert action to PDF object
582    fn action_to_object(&self, action: &FieldAction) -> Object {
583        let mut dict = Dictionary::new();
584
585        match action {
586            FieldAction::JavaScript { script, .. } => {
587                dict.set("S", Object::Name("JavaScript".to_string()));
588                dict.set("JS", Object::String(script.clone()));
589            }
590            FieldAction::SubmitForm { url, .. } => {
591                dict.set("S", Object::Name("SubmitForm".to_string()));
592                dict.set("F", Object::String(url.clone()));
593            }
594            FieldAction::ResetForm { .. } => {
595                dict.set("S", Object::Name("ResetForm".to_string()));
596            }
597            _ => {
598                dict.set("S", Object::Name("Unknown".to_string()));
599            }
600        }
601
602        Object::Dictionary(dict)
603    }
604}
605
606impl fmt::Display for ActionEvent {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        write!(
609            f,
610            "[{}] Field '{}': {:?} -> {:?}",
611            self.timestamp.format("%H:%M:%S"),
612            self.field_name,
613            self.event_type,
614            self.result
615        )
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    #[test]
624    fn test_focus_blur_events() {
625        let mut system = FieldActionSystem::new();
626
627        let actions = FieldActions {
628            on_focus: Some(FieldAction::JavaScript {
629                script: "console.log('Field focused');".to_string(),
630                async_exec: false,
631            }),
632            on_blur: Some(FieldAction::JavaScript {
633                script: "console.log('Field blurred');".to_string(),
634                async_exec: false,
635            }),
636            ..Default::default()
637        };
638
639        system.register_field_actions("test_field", actions);
640
641        // Test focus
642        system.handle_focus("test_field").unwrap();
643        assert_eq!(system.get_focused_field(), Some(&"test_field".to_string()));
644
645        // Test blur
646        system.handle_blur("test_field").unwrap();
647        assert_eq!(system.get_focused_field(), None);
648    }
649
650    #[test]
651    fn test_field_switching() {
652        let mut system = FieldActionSystem::new();
653
654        // Register actions for two fields
655        for field in ["field1", "field2"] {
656            let actions = FieldActions {
657                on_focus: Some(FieldAction::SetField {
658                    target_field: "status".to_string(),
659                    value: FieldValue::Text(format!("{} focused", field)),
660                }),
661                on_blur: Some(FieldAction::SetField {
662                    target_field: "status".to_string(),
663                    value: FieldValue::Text(format!("{} blurred", field)),
664                }),
665                ..Default::default()
666            };
667            system.register_field_actions(field, actions);
668        }
669
670        // Focus field1
671        system.handle_focus("field1").unwrap();
672        assert_eq!(system.get_focused_field(), Some(&"field1".to_string()));
673
674        // Switch to field2 (should blur field1 and focus field2)
675        system.handle_focus("field2").unwrap();
676        assert_eq!(system.get_focused_field(), Some(&"field2".to_string()));
677    }
678
679    #[test]
680    fn test_validate_action() {
681        let mut system = FieldActionSystem::new();
682
683        let actions = FieldActions {
684            on_validate: Some(FieldAction::Validate {
685                validation_type: ValidateActionType::Range {
686                    min: Some(0.0),
687                    max: Some(100.0),
688                },
689            }),
690            ..Default::default()
691        };
692
693        system.register_field_actions("score", actions);
694
695        // Test validation
696        let valid = system
697            .handle_validate("score", &FieldValue::Number(50.0))
698            .unwrap();
699        assert!(valid);
700    }
701
702    #[test]
703    fn test_event_history() {
704        let mut system = FieldActionSystem::new();
705
706        let actions = FieldActions {
707            on_focus: Some(FieldAction::ShowHide {
708                fields: vec!["help_text".to_string()],
709                show: true,
710            }),
711            on_blur: Some(FieldAction::ShowHide {
712                fields: vec!["help_text".to_string()],
713                show: false,
714            }),
715            ..Default::default()
716        };
717
718        system.register_field_actions("field1", actions);
719
720        // Trigger events
721        system.handle_focus("field1").unwrap();
722        system.handle_blur("field1").unwrap();
723
724        // Check history
725        assert_eq!(system.get_event_history().len(), 2);
726        assert_eq!(
727            system.get_event_history()[0].event_type,
728            ActionEventType::Focus
729        );
730        assert_eq!(
731            system.get_event_history()[1].event_type,
732            ActionEventType::Blur
733        );
734    }
735
736    #[test]
737    fn test_format_action_types() {
738        // Test Number format
739        let number_format = FormatActionType::Number {
740            decimals: 2,
741            currency: Some("USD".to_string()),
742        };
743
744        match number_format {
745            FormatActionType::Number { decimals, currency } => {
746                assert_eq!(decimals, 2);
747                assert_eq!(currency, Some("USD".to_string()));
748            }
749            _ => panic!("Expected Number format"),
750        }
751
752        // Test Percent format
753        let percent_format = FormatActionType::Percent { decimals: 1 };
754
755        match percent_format {
756            FormatActionType::Percent { decimals } => assert_eq!(decimals, 1),
757            _ => panic!("Expected Percent format"),
758        }
759
760        // Test Date format
761        let date_format = FormatActionType::Date {
762            format: "mm/dd/yyyy".to_string(),
763        };
764
765        match date_format {
766            FormatActionType::Date { format } => assert_eq!(format, "mm/dd/yyyy"),
767            _ => panic!("Expected Date format"),
768        }
769
770        // Test Special format
771        let special_format = FormatActionType::Special {
772            format: SpecialFormatType::ZipCode,
773        };
774
775        match special_format {
776            FormatActionType::Special { format } => {
777                matches!(format, SpecialFormatType::ZipCode);
778            }
779            _ => panic!("Expected Special format"),
780        }
781    }
782
783    #[test]
784    fn test_validate_action_types() {
785        // Test Range validation
786        let range_validate = ValidateActionType::Range {
787            min: Some(0.0),
788            max: Some(100.0),
789        };
790
791        match range_validate {
792            ValidateActionType::Range { min, max } => {
793                assert_eq!(min, Some(0.0));
794                assert_eq!(max, Some(100.0));
795            }
796            _ => panic!("Expected Range validation"),
797        }
798
799        // Test Custom validation
800        let custom_validate = ValidateActionType::Custom {
801            script: "return value > 0;".to_string(),
802        };
803
804        match custom_validate {
805            ValidateActionType::Custom { script } => {
806                assert!(script.contains("return"));
807            }
808            _ => panic!("Expected Custom validation"),
809        }
810    }
811
812    #[test]
813    fn test_action_result() {
814        // Test Success
815        let success_result = ActionResult::Success;
816        matches!(success_result, ActionResult::Success);
817
818        // Test Failed
819        let failed_result = ActionResult::Failed("Test error".to_string());
820        match failed_result {
821            ActionResult::Failed(msg) => assert_eq!(msg, "Test error"),
822            _ => panic!("Expected Failed result"),
823        }
824
825        // Test Cancelled
826        let cancelled_result = ActionResult::Cancelled;
827        matches!(cancelled_result, ActionResult::Cancelled);
828
829        // Test Modified
830        let modified_result = ActionResult::Modified(FieldValue::Text("Modified".to_string()));
831        match modified_result {
832            ActionResult::Modified(value) => {
833                assert_eq!(value, FieldValue::Text("Modified".to_string()));
834            }
835            _ => panic!("Expected Modified result"),
836        }
837    }
838
839    #[test]
840    fn test_action_settings() {
841        let settings = ActionSettings::default();
842        assert!(settings.enable_javascript);
843        assert!(!settings.enable_form_submit); // Default is false
844        assert!(settings.enable_sound);
845        assert!(settings.log_events);
846        assert_eq!(settings.max_event_history, 1000);
847
848        // Test with custom settings
849        let custom_settings = ActionSettings {
850            enable_javascript: false,
851            enable_form_submit: true,
852            enable_sound: false,
853            log_events: false,
854            max_event_history: 500,
855        };
856        assert!(!custom_settings.enable_javascript);
857        assert!(custom_settings.enable_form_submit);
858        assert!(!custom_settings.enable_sound);
859        assert!(!custom_settings.log_events);
860        assert_eq!(custom_settings.max_event_history, 500);
861    }
862
863    #[test]
864    fn test_field_action_system_settings() {
865        let system = FieldActionSystem::new();
866
867        // Test default settings
868        assert!(system.settings.enable_javascript);
869        assert!(!system.settings.enable_form_submit);
870        assert!(system.settings.enable_sound);
871        assert!(system.settings.log_events);
872        assert_eq!(system.settings.max_event_history, 1000);
873
874        // Create with custom settings
875        let custom_settings = ActionSettings {
876            enable_javascript: false,
877            enable_form_submit: true,
878            enable_sound: false,
879            log_events: false,
880            max_event_history: 100,
881        };
882        let system_with_settings = FieldActionSystem::with_settings(custom_settings);
883        assert!(!system_with_settings.settings.enable_javascript);
884        assert!(system_with_settings.settings.enable_form_submit);
885    }
886
887    #[test]
888    fn test_clear_event_history() {
889        let mut system = FieldActionSystem::new();
890
891        // Add some events
892        let actions = FieldActions {
893            on_focus: Some(FieldAction::Custom {
894                action_type: "test".to_string(),
895                parameters: HashMap::new(),
896            }),
897            ..Default::default()
898        };
899
900        system.register_field_actions("field1", actions);
901        system.handle_focus("field1").unwrap();
902
903        assert!(system.get_event_history().len() > 0);
904
905        // Clear history
906        system.clear_event_history();
907        assert_eq!(system.get_event_history().len(), 0);
908    }
909
910    #[test]
911    fn test_mouse_actions() {
912        let mut system = FieldActionSystem::new();
913
914        let actions = FieldActions {
915            on_mouse_enter: Some(FieldAction::Custom {
916                action_type: "highlight".to_string(),
917                parameters: HashMap::from([("color".to_string(), "yellow".to_string())]),
918            }),
919            on_mouse_exit: Some(FieldAction::Custom {
920                action_type: "unhighlight".to_string(),
921                parameters: HashMap::new(),
922            }),
923            on_mouse_down: Some(FieldAction::Custom {
924                action_type: "pressed".to_string(),
925                parameters: HashMap::new(),
926            }),
927            on_mouse_up: Some(FieldAction::Custom {
928                action_type: "released".to_string(),
929                parameters: HashMap::new(),
930            }),
931            ..Default::default()
932        };
933
934        system.register_field_actions("button1", actions);
935
936        // Mouse events aren't directly handled - they would be triggered through
937        // focus/blur or other events. Just verify the actions were registered.
938        assert!(system.actions.contains_key("button1"));
939        let registered_actions = &system.actions["button1"];
940        assert!(registered_actions.on_mouse_enter.is_some());
941        assert!(registered_actions.on_mouse_exit.is_some());
942        assert!(registered_actions.on_mouse_down.is_some());
943        assert!(registered_actions.on_mouse_up.is_some());
944    }
945
946    #[test]
947    fn test_submit_form_action() {
948        let action = FieldAction::SubmitForm {
949            url: "https://example.com/submit".to_string(),
950            fields: vec!["name".to_string(), "email".to_string()],
951            include_empty: false,
952        };
953
954        match action {
955            FieldAction::SubmitForm {
956                url,
957                fields,
958                include_empty,
959            } => {
960                assert_eq!(url, "https://example.com/submit");
961                assert_eq!(fields.len(), 2);
962                assert!(!include_empty);
963            }
964            _ => panic!("Expected SubmitForm action"),
965        }
966    }
967
968    #[test]
969    fn test_reset_form_action() {
970        let action = FieldAction::ResetForm {
971            fields: vec!["field1".to_string(), "field2".to_string()],
972            exclude: true,
973        };
974
975        match action {
976            FieldAction::ResetForm { fields, exclude } => {
977                assert_eq!(fields.len(), 2);
978                assert!(exclude);
979            }
980            _ => panic!("Expected ResetForm action"),
981        }
982    }
983
984    #[test]
985    fn test_field_value_action() {
986        let action = FieldAction::SetField {
987            target_field: "total".to_string(),
988            value: FieldValue::Number(100.0),
989        };
990
991        match action {
992            FieldAction::SetField {
993                target_field,
994                value,
995            } => {
996                assert_eq!(target_field, "total");
997                assert_eq!(value, FieldValue::Number(100.0));
998            }
999            _ => panic!("Expected SetField action"),
1000        }
1001    }
1002
1003    #[test]
1004    fn test_action_event_types() {
1005        // Test enum variants
1006        let focus = ActionEventType::Focus;
1007        let blur = ActionEventType::Blur;
1008        let format = ActionEventType::Format;
1009        let keystroke = ActionEventType::Keystroke;
1010        let calculate = ActionEventType::Calculate;
1011        let validate = ActionEventType::Validate;
1012
1013        // Test equality
1014        assert_eq!(focus, ActionEventType::Focus);
1015        assert_eq!(blur, ActionEventType::Blur);
1016        assert_eq!(format, ActionEventType::Format);
1017        assert_eq!(keystroke, ActionEventType::Keystroke);
1018        assert_eq!(calculate, ActionEventType::Calculate);
1019        assert_eq!(validate, ActionEventType::Validate);
1020        assert_ne!(focus, blur);
1021    }
1022
1023    #[test]
1024    fn test_special_format_types() {
1025        let zip = SpecialFormatType::ZipCode;
1026        let zip_plus = SpecialFormatType::ZipPlus4;
1027        let phone = SpecialFormatType::Phone;
1028        let ssn = SpecialFormatType::SSN;
1029
1030        // Just verify they exist and can be created
1031        matches!(zip, SpecialFormatType::ZipCode);
1032        matches!(zip_plus, SpecialFormatType::ZipPlus4);
1033        matches!(phone, SpecialFormatType::Phone);
1034        matches!(ssn, SpecialFormatType::SSN);
1035    }
1036
1037    #[test]
1038    fn test_play_sound_action() {
1039        let action = FieldAction::PlaySound {
1040            sound_name: "beep".to_string(),
1041            volume: 0.5,
1042        };
1043
1044        match action {
1045            FieldAction::PlaySound { sound_name, volume } => {
1046                assert_eq!(sound_name, "beep");
1047                assert_eq!(volume, 0.5);
1048            }
1049            _ => panic!("Expected PlaySound action"),
1050        }
1051    }
1052
1053    #[test]
1054    fn test_import_data_action() {
1055        let action = FieldAction::ImportData {
1056            file_path: "/path/to/data.fdf".to_string(),
1057        };
1058
1059        match action {
1060            FieldAction::ImportData { file_path } => {
1061                assert_eq!(file_path, "/path/to/data.fdf");
1062            }
1063            _ => panic!("Expected ImportData action"),
1064        }
1065    }
1066
1067    #[test]
1068    fn test_show_hide_action() {
1069        let action = FieldAction::ShowHide {
1070            fields: vec!["field1".to_string(), "field2".to_string()],
1071            show: true,
1072        };
1073
1074        match action {
1075            FieldAction::ShowHide { fields, show } => {
1076                assert_eq!(fields.len(), 2);
1077                assert!(show);
1078            }
1079            _ => panic!("Expected ShowHide action"),
1080        }
1081    }
1082
1083    #[test]
1084    fn test_custom_action() {
1085        let mut params = HashMap::new();
1086        params.insert("key1".to_string(), "value1".to_string());
1087        params.insert("key2".to_string(), "value2".to_string());
1088
1089        let action = FieldAction::Custom {
1090            action_type: "custom_type".to_string(),
1091            parameters: params.clone(),
1092        };
1093
1094        match action {
1095            FieldAction::Custom {
1096                action_type,
1097                parameters,
1098            } => {
1099                assert_eq!(action_type, "custom_type");
1100                assert_eq!(parameters.len(), 2);
1101                assert_eq!(parameters.get("key1"), Some(&"value1".to_string()));
1102            }
1103            _ => panic!("Expected Custom action"),
1104        }
1105    }
1106}