jugar_probar/brick/
event.rs

1//! EventBrick: DOM event handler generation from brick definitions (PROBAR-SPEC-009-P7)
2//!
3//! Generates JavaScript event handlers from brick definitions.
4//! Zero hand-written event handling code.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use probar::brick::event::{EventBrick, EventType, EventHandler};
10//!
11//! let events = EventBrick::new()
12//!     .on("#record", EventType::Click, EventHandler::dispatch_state("toggle_recording"))
13//!     .on("#clear", EventType::Click, EventHandler::call_wasm("clear_transcript"));
14//!
15//! let js = events.to_event_js();
16//! ```
17
18use super::{Brick, BrickAssertion, BrickBudget, BrickVerification};
19use std::time::Duration;
20
21/// DOM event types
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum EventType {
24    /// Mouse click
25    Click,
26    /// Double click
27    DoubleClick,
28    /// Mouse down
29    MouseDown,
30    /// Mouse up
31    MouseUp,
32    /// Mouse enter (hover start)
33    MouseEnter,
34    /// Mouse leave (hover end)
35    MouseLeave,
36    /// Key down
37    KeyDown,
38    /// Key up
39    KeyUp,
40    /// Key press
41    KeyPress,
42    /// Input value change
43    Input,
44    /// Form change
45    Change,
46    /// Form submit
47    Submit,
48    /// Focus gained
49    Focus,
50    /// Focus lost
51    Blur,
52    /// Scroll
53    Scroll,
54    /// Touch start
55    TouchStart,
56    /// Touch end
57    TouchEnd,
58    /// Touch move
59    TouchMove,
60    /// Custom event
61    Custom(&'static str),
62}
63
64impl EventType {
65    /// Get the JavaScript event name
66    #[must_use]
67    pub fn js_name(&self) -> &str {
68        match self {
69            Self::Click => "click",
70            Self::DoubleClick => "dblclick",
71            Self::MouseDown => "mousedown",
72            Self::MouseUp => "mouseup",
73            Self::MouseEnter => "mouseenter",
74            Self::MouseLeave => "mouseleave",
75            Self::KeyDown => "keydown",
76            Self::KeyUp => "keyup",
77            Self::KeyPress => "keypress",
78            Self::Input => "input",
79            Self::Change => "change",
80            Self::Submit => "submit",
81            Self::Focus => "focus",
82            Self::Blur => "blur",
83            Self::Scroll => "scroll",
84            Self::TouchStart => "touchstart",
85            Self::TouchEnd => "touchend",
86            Self::TouchMove => "touchmove",
87            Self::Custom(name) => name,
88        }
89    }
90}
91
92/// Event handler action
93#[derive(Debug, Clone)]
94pub enum EventHandler {
95    /// Dispatch a state change event
96    DispatchState(String),
97
98    /// Call a WASM exported function
99    CallWasm {
100        /// Function name
101        function: String,
102        /// Arguments to pass (JavaScript expressions)
103        args: Vec<String>,
104    },
105
106    /// Post a message to a worker
107    PostMessage {
108        /// Target worker name
109        worker: String,
110        /// Message type
111        message_type: String,
112        /// Message fields (key = field name, value = JS expression)
113        fields: Vec<(String, String)>,
114    },
115
116    /// Update a DOM element
117    UpdateElement {
118        /// Target selector
119        selector: String,
120        /// Property to update
121        property: String,
122        /// New value (JavaScript expression)
123        value: String,
124    },
125
126    /// Toggle a CSS class
127    ToggleClass {
128        /// Target selector
129        selector: String,
130        /// Class name
131        class: String,
132    },
133
134    /// Prevent default and stop propagation
135    PreventDefault,
136
137    /// Chain multiple handlers
138    Chain(Vec<EventHandler>),
139
140    /// Conditional handler
141    If {
142        /// Condition (JavaScript expression)
143        condition: String,
144        /// Handler if true
145        then: Box<EventHandler>,
146        /// Handler if false (optional)
147        otherwise: Option<Box<EventHandler>>,
148    },
149}
150
151impl EventHandler {
152    /// Create a state dispatch handler
153    #[must_use]
154    pub fn dispatch_state(state: impl Into<String>) -> Self {
155        Self::DispatchState(state.into())
156    }
157
158    /// Create a WASM call handler
159    #[must_use]
160    pub fn call_wasm(function: impl Into<String>) -> Self {
161        Self::CallWasm {
162            function: function.into(),
163            args: Vec::new(),
164        }
165    }
166
167    /// Create a WASM call handler with arguments
168    #[must_use]
169    pub fn call_wasm_with_args(function: impl Into<String>, args: Vec<String>) -> Self {
170        Self::CallWasm {
171            function: function.into(),
172            args,
173        }
174    }
175
176    /// Create a worker message handler
177    #[must_use]
178    pub fn post_to_worker(worker: impl Into<String>, message_type: impl Into<String>) -> Self {
179        Self::PostMessage {
180            worker: worker.into(),
181            message_type: message_type.into(),
182            fields: Vec::new(),
183        }
184    }
185
186    /// Create an element update handler
187    #[must_use]
188    pub fn update_element(
189        selector: impl Into<String>,
190        property: impl Into<String>,
191        value: impl Into<String>,
192    ) -> Self {
193        Self::UpdateElement {
194            selector: selector.into(),
195            property: property.into(),
196            value: value.into(),
197        }
198    }
199
200    /// Create a class toggle handler
201    #[must_use]
202    pub fn toggle_class(selector: impl Into<String>, class: impl Into<String>) -> Self {
203        Self::ToggleClass {
204            selector: selector.into(),
205            class: class.into(),
206        }
207    }
208
209    /// Chain handlers
210    #[must_use]
211    pub fn chain(handlers: Vec<EventHandler>) -> Self {
212        Self::Chain(handlers)
213    }
214
215    /// Create a conditional handler
216    #[must_use]
217    pub fn when(
218        condition: impl Into<String>,
219        then: EventHandler,
220        otherwise: Option<EventHandler>,
221    ) -> Self {
222        Self::If {
223            condition: condition.into(),
224            then: Box::new(then),
225            otherwise: otherwise.map(Box::new),
226        }
227    }
228
229    /// Generate JavaScript code for this handler
230    #[must_use]
231    pub fn to_js(&self, indent: usize) -> String {
232        let pad = "    ".repeat(indent);
233
234        match self {
235            Self::DispatchState(state) => {
236                format!(
237                    "{}window.dispatchEvent(new CustomEvent('state-change', {{ detail: '{}' }}));",
238                    pad, state
239                )
240            }
241
242            Self::CallWasm { function, args } => {
243                let args_str = args.join(", ");
244                format!("{}window.wasm.{}({});", pad, function, args_str)
245            }
246
247            Self::PostMessage {
248                worker,
249                message_type,
250                fields,
251            } => {
252                let fields_str = if fields.is_empty() {
253                    String::new()
254                } else {
255                    let f: Vec<_> = fields
256                        .iter()
257                        .map(|(k, v)| format!("{}: {}", k, v))
258                        .collect();
259                    format!(", {}", f.join(", "))
260                };
261                format!(
262                    "{}{}.postMessage({{ type: '{}'{} }});",
263                    pad, worker, message_type, fields_str
264                )
265            }
266
267            Self::UpdateElement {
268                selector,
269                property,
270                value,
271            } => {
272                format!(
273                    "{}document.querySelector('{}').{} = {};",
274                    pad, selector, property, value
275                )
276            }
277
278            Self::ToggleClass { selector, class } => {
279                format!(
280                    "{}document.querySelector('{}').classList.toggle('{}');",
281                    pad, selector, class
282                )
283            }
284
285            Self::PreventDefault => {
286                format!("{}e.preventDefault();\n{}e.stopPropagation();", pad, pad)
287            }
288
289            Self::Chain(handlers) => handlers
290                .iter()
291                .map(|h| h.to_js(indent))
292                .collect::<Vec<_>>()
293                .join("\n"),
294
295            Self::If {
296                condition,
297                then,
298                otherwise,
299            } => {
300                let then_js = then.to_js(indent + 1);
301                let else_js = otherwise
302                    .as_ref()
303                    .map(|h| format!(" else {{\n{}\n{}}}", h.to_js(indent + 1), pad))
304                    .unwrap_or_default();
305
306                format!(
307                    "{}if ({}) {{\n{}\n{}}}{}",
308                    pad, condition, then_js, pad, else_js
309                )
310            }
311        }
312    }
313}
314
315/// A single event binding
316#[derive(Debug, Clone)]
317pub struct EventBinding {
318    /// CSS selector for the target element
319    pub selector: String,
320    /// Event type to listen for
321    pub event_type: EventType,
322    /// Handler to execute
323    pub handler: EventHandler,
324    /// Use capture phase
325    pub capture: bool,
326    /// Only fire once
327    pub once: bool,
328    /// Passive listener (performance optimization)
329    pub passive: bool,
330}
331
332impl EventBinding {
333    /// Create a new event binding
334    #[must_use]
335    pub fn new(selector: impl Into<String>, event_type: EventType, handler: EventHandler) -> Self {
336        Self {
337            selector: selector.into(),
338            event_type,
339            handler,
340            capture: false,
341            once: false,
342            passive: false,
343        }
344    }
345
346    /// Use capture phase
347    #[must_use]
348    pub fn capture(mut self) -> Self {
349        self.capture = true;
350        self
351    }
352
353    /// Only fire once
354    #[must_use]
355    pub fn once(mut self) -> Self {
356        self.once = true;
357        self
358    }
359
360    /// Mark as passive (for scroll/touch performance)
361    #[must_use]
362    pub fn passive(mut self) -> Self {
363        self.passive = true;
364        self
365    }
366
367    /// Generate JavaScript for this binding
368    #[must_use]
369    pub fn to_js(&self) -> String {
370        let handler_js = self.handler.to_js(2);
371
372        let options = if self.capture || self.once || self.passive {
373            let mut opts = Vec::new();
374            if self.capture {
375                opts.push("capture: true");
376            }
377            if self.once {
378                opts.push("once: true");
379            }
380            if self.passive {
381                opts.push("passive: true");
382            }
383            format!(", {{ {} }}", opts.join(", "))
384        } else {
385            String::new()
386        };
387
388        format!(
389            "document.querySelector('{}').addEventListener('{}', (e) => {{\n{}\n}}{}); ",
390            self.selector,
391            self.event_type.js_name(),
392            handler_js,
393            options
394        )
395    }
396}
397
398/// EventBrick: Generates DOM event handlers from brick definition
399#[derive(Debug, Clone, Default)]
400pub struct EventBrick {
401    /// Event bindings
402    bindings: Vec<EventBinding>,
403    /// Global window event handlers
404    window_handlers: Vec<(EventType, EventHandler)>,
405}
406
407impl EventBrick {
408    /// Create a new event brick
409    #[must_use]
410    pub fn new() -> Self {
411        Self::default()
412    }
413
414    /// Add an event binding
415    #[must_use]
416    pub fn on(
417        mut self,
418        selector: impl Into<String>,
419        event_type: EventType,
420        handler: EventHandler,
421    ) -> Self {
422        self.bindings
423            .push(EventBinding::new(selector, event_type, handler));
424        self
425    }
426
427    /// Add an event binding with options
428    #[must_use]
429    pub fn on_with(mut self, binding: EventBinding) -> Self {
430        self.bindings.push(binding);
431        self
432    }
433
434    /// Add a window-level event handler
435    #[must_use]
436    pub fn on_window(mut self, event_type: EventType, handler: EventHandler) -> Self {
437        self.window_handlers.push((event_type, handler));
438        self
439    }
440
441    /// Generate JavaScript for all event handlers
442    #[must_use]
443    pub fn to_event_js(&self) -> String {
444        let mut js = String::new();
445
446        js.push_str("// Event Handlers\n");
447        js.push_str("// Generated by probar - DO NOT EDIT MANUALLY\n\n");
448
449        // Element bindings
450        for binding in &self.bindings {
451            js.push_str(&binding.to_js());
452            js.push('\n');
453        }
454
455        // Window handlers
456        for (event_type, handler) in &self.window_handlers {
457            let handler_js = handler.to_js(1);
458            js.push_str(&format!(
459                "window.addEventListener('{}', (e) => {{\n{}\n}});\n",
460                event_type.js_name(),
461                handler_js
462            ));
463        }
464
465        js
466    }
467
468    /// Get all selectors referenced by this brick
469    #[must_use]
470    pub fn selectors(&self) -> Vec<&str> {
471        self.bindings.iter().map(|b| b.selector.as_str()).collect()
472    }
473}
474
475impl Brick for EventBrick {
476    fn brick_name(&self) -> &'static str {
477        "EventBrick"
478    }
479
480    fn assertions(&self) -> &[BrickAssertion] {
481        &[]
482    }
483
484    fn budget(&self) -> BrickBudget {
485        BrickBudget::uniform(100)
486    }
487
488    fn verify(&self) -> BrickVerification {
489        let passed = vec![BrickAssertion::Custom {
490            name: "event_bindings_valid".into(),
491            validator_id: 10,
492        }];
493
494        BrickVerification {
495            passed,
496            failed: Vec::new(),
497            verification_time: Duration::from_micros(50),
498        }
499    }
500
501    fn to_html(&self) -> String {
502        String::new()
503    }
504
505    fn to_css(&self) -> String {
506        String::new()
507    }
508}
509
510#[cfg(test)]
511#[allow(clippy::unwrap_used, clippy::expect_used)]
512mod tests {
513    use super::*;
514
515    // ============================================================
516    // EventType tests
517    // ============================================================
518
519    #[test]
520    fn test_event_type_js_name() {
521        assert_eq!(EventType::Click.js_name(), "click");
522        assert_eq!(EventType::KeyDown.js_name(), "keydown");
523        assert_eq!(EventType::Custom("my-event").js_name(), "my-event");
524    }
525
526    #[test]
527    fn test_event_type_js_name_all_variants() {
528        // Test all EventType variants for js_name
529        assert_eq!(EventType::Click.js_name(), "click");
530        assert_eq!(EventType::DoubleClick.js_name(), "dblclick");
531        assert_eq!(EventType::MouseDown.js_name(), "mousedown");
532        assert_eq!(EventType::MouseUp.js_name(), "mouseup");
533        assert_eq!(EventType::MouseEnter.js_name(), "mouseenter");
534        assert_eq!(EventType::MouseLeave.js_name(), "mouseleave");
535        assert_eq!(EventType::KeyDown.js_name(), "keydown");
536        assert_eq!(EventType::KeyUp.js_name(), "keyup");
537        assert_eq!(EventType::KeyPress.js_name(), "keypress");
538        assert_eq!(EventType::Input.js_name(), "input");
539        assert_eq!(EventType::Change.js_name(), "change");
540        assert_eq!(EventType::Submit.js_name(), "submit");
541        assert_eq!(EventType::Focus.js_name(), "focus");
542        assert_eq!(EventType::Blur.js_name(), "blur");
543        assert_eq!(EventType::Scroll.js_name(), "scroll");
544        assert_eq!(EventType::TouchStart.js_name(), "touchstart");
545        assert_eq!(EventType::TouchEnd.js_name(), "touchend");
546        assert_eq!(EventType::TouchMove.js_name(), "touchmove");
547        assert_eq!(EventType::Custom("custom-event").js_name(), "custom-event");
548    }
549
550    #[test]
551    fn test_event_type_debug_and_clone() {
552        let event = EventType::Click;
553        let cloned = event;
554        assert_eq!(format!("{:?}", cloned), "Click");
555
556        let custom = EventType::Custom("test");
557        let custom_clone = custom;
558        assert_eq!(custom_clone.js_name(), "test");
559    }
560
561    #[test]
562    fn test_event_type_equality() {
563        assert_eq!(EventType::Click, EventType::Click);
564        assert_ne!(EventType::Click, EventType::DoubleClick);
565        assert_eq!(EventType::Custom("a"), EventType::Custom("a"));
566        assert_ne!(EventType::Custom("a"), EventType::Custom("b"));
567    }
568
569    // ============================================================
570    // EventHandler tests
571    // ============================================================
572
573    #[test]
574    fn test_event_handler_dispatch_state() {
575        let handler = EventHandler::dispatch_state("recording");
576        let js = handler.to_js(0);
577
578        assert!(js.contains("dispatchEvent"));
579        assert!(js.contains("state-change"));
580        assert!(js.contains("recording"));
581    }
582
583    #[test]
584    fn test_event_handler_call_wasm() {
585        let handler = EventHandler::call_wasm("start_recording");
586        let js = handler.to_js(0);
587
588        assert!(js.contains("window.wasm.start_recording()"));
589    }
590
591    #[test]
592    fn test_event_handler_call_wasm_with_args() {
593        let handler = EventHandler::call_wasm_with_args(
594            "process_data",
595            vec!["arg1".to_string(), "arg2".to_string(), "123".to_string()],
596        );
597        let js = handler.to_js(0);
598
599        assert!(js.contains("window.wasm.process_data(arg1, arg2, 123)"));
600    }
601
602    #[test]
603    fn test_event_handler_call_wasm_with_empty_args() {
604        let handler = EventHandler::call_wasm_with_args("func", vec![]);
605        let js = handler.to_js(0);
606
607        assert!(js.contains("window.wasm.func()"));
608    }
609
610    #[test]
611    fn test_event_handler_post_to_worker() {
612        let handler = EventHandler::post_to_worker("myWorker", "start");
613        let js = handler.to_js(0);
614
615        assert!(js.contains("myWorker.postMessage"));
616        assert!(js.contains("type: 'start'"));
617    }
618
619    #[test]
620    fn test_event_handler_post_message_with_fields() {
621        let handler = EventHandler::PostMessage {
622            worker: "worker".to_string(),
623            message_type: "update".to_string(),
624            fields: vec![
625                ("data".to_string(), "e.target.value".to_string()),
626                ("count".to_string(), "42".to_string()),
627            ],
628        };
629        let js = handler.to_js(0);
630
631        assert!(js.contains("worker.postMessage"));
632        assert!(js.contains("type: 'update'"));
633        assert!(js.contains("data: e.target.value"));
634        assert!(js.contains("count: 42"));
635    }
636
637    #[test]
638    fn test_event_handler_update_element() {
639        let handler = EventHandler::update_element("#status", "textContent", "'Ready'");
640        let js = handler.to_js(0);
641
642        assert!(js.contains("#status"));
643        assert!(js.contains("textContent"));
644        assert!(js.contains("'Ready'"));
645        assert!(js.contains("querySelector"));
646    }
647
648    #[test]
649    fn test_event_handler_toggle_class() {
650        let handler = EventHandler::toggle_class("#menu", "active");
651        let js = handler.to_js(0);
652
653        assert!(js.contains("querySelector('#menu')"));
654        assert!(js.contains("classList.toggle('active')"));
655    }
656
657    #[test]
658    fn test_event_handler_prevent_default() {
659        let js = EventHandler::PreventDefault.to_js(0);
660
661        assert!(js.contains("e.preventDefault()"));
662        assert!(js.contains("e.stopPropagation()"));
663    }
664
665    #[test]
666    fn test_event_handler_chain() {
667        let handler = EventHandler::chain(vec![
668            EventHandler::PreventDefault,
669            EventHandler::dispatch_state("clicked"),
670        ]);
671
672        let js = handler.to_js(0);
673
674        assert!(js.contains("preventDefault"));
675        assert!(js.contains("dispatchEvent"));
676    }
677
678    #[test]
679    fn test_event_handler_chain_empty() {
680        let handler = EventHandler::chain(vec![]);
681        let js = handler.to_js(0);
682        assert!(js.is_empty());
683    }
684
685    #[test]
686    fn test_event_handler_chain_multiple() {
687        let handler = EventHandler::chain(vec![
688            EventHandler::PreventDefault,
689            EventHandler::dispatch_state("state1"),
690            EventHandler::call_wasm("func1"),
691            EventHandler::toggle_class("#el", "class1"),
692        ]);
693
694        let js = handler.to_js(0);
695
696        assert!(js.contains("preventDefault"));
697        assert!(js.contains("state1"));
698        assert!(js.contains("func1"));
699        assert!(js.contains("class1"));
700    }
701
702    #[test]
703    fn test_event_handler_conditional() {
704        let handler = EventHandler::when(
705            "isRecording",
706            EventHandler::dispatch_state("stop"),
707            Some(EventHandler::dispatch_state("start")),
708        );
709
710        let js = handler.to_js(0);
711
712        assert!(js.contains("if (isRecording)"));
713        assert!(js.contains("stop"));
714        assert!(js.contains("else"));
715        assert!(js.contains("start"));
716    }
717
718    #[test]
719    fn test_event_handler_conditional_without_else() {
720        let handler = EventHandler::when("condition", EventHandler::call_wasm("action"), None);
721
722        let js = handler.to_js(0);
723
724        assert!(js.contains("if (condition)"));
725        assert!(js.contains("action"));
726        assert!(!js.contains("else"));
727    }
728
729    #[test]
730    fn test_event_handler_to_js_with_indent() {
731        let handler = EventHandler::dispatch_state("test");
732
733        let js_0 = handler.to_js(0);
734        let js_1 = handler.to_js(1);
735        let js_2 = handler.to_js(2);
736
737        assert!(!js_0.starts_with(' '));
738        assert!(js_1.starts_with("    "));
739        assert!(js_2.starts_with("        "));
740    }
741
742    #[test]
743    fn test_event_handler_debug_and_clone() {
744        let handler = EventHandler::dispatch_state("test");
745        let cloned = handler;
746
747        assert!(format!("{:?}", cloned).contains("DispatchState"));
748    }
749
750    // ============================================================
751    // EventBinding tests
752    // ============================================================
753
754    #[test]
755    fn test_event_binding_basic() {
756        let binding = EventBinding::new(
757            "#button",
758            EventType::Click,
759            EventHandler::dispatch_state("clicked"),
760        );
761
762        let js = binding.to_js();
763
764        assert!(js.contains("#button"));
765        assert!(js.contains("click"));
766        assert!(js.contains("addEventListener"));
767    }
768
769    #[test]
770    fn test_event_binding_options() {
771        let binding = EventBinding::new(
772            "#scroll",
773            EventType::Scroll,
774            EventHandler::call_wasm("on_scroll"),
775        )
776        .passive()
777        .capture();
778
779        let js = binding.to_js();
780
781        assert!(js.contains("passive: true"));
782        assert!(js.contains("capture: true"));
783    }
784
785    #[test]
786    fn test_event_binding_once() {
787        let binding = EventBinding::new(
788            "#init",
789            EventType::Click,
790            EventHandler::call_wasm("initialize"),
791        )
792        .once();
793
794        let js = binding.to_js();
795
796        assert!(js.contains("once: true"));
797    }
798
799    #[test]
800    fn test_event_binding_all_options() {
801        let binding = EventBinding::new(
802            "#element",
803            EventType::TouchStart,
804            EventHandler::PreventDefault,
805        )
806        .capture()
807        .once()
808        .passive();
809
810        let js = binding.to_js();
811
812        assert!(js.contains("capture: true"));
813        assert!(js.contains("once: true"));
814        assert!(js.contains("passive: true"));
815    }
816
817    #[test]
818    fn test_event_binding_no_options() {
819        let binding = EventBinding::new(
820            "#simple",
821            EventType::Click,
822            EventHandler::dispatch_state("click"),
823        );
824
825        let js = binding.to_js();
826
827        // Should not contain options object when no options set
828        assert!(!js.contains("capture:"));
829        assert!(!js.contains("once:"));
830        assert!(!js.contains("passive:"));
831    }
832
833    #[test]
834    fn test_event_binding_debug_and_clone() {
835        let binding = EventBinding::new(
836            "#test",
837            EventType::Click,
838            EventHandler::dispatch_state("test"),
839        );
840        let cloned = binding;
841
842        assert_eq!(cloned.selector, "#test");
843        assert!(format!("{:?}", cloned).contains("EventBinding"));
844    }
845
846    #[test]
847    fn test_event_binding_fields() {
848        let binding = EventBinding::new(
849            "#target",
850            EventType::MouseEnter,
851            EventHandler::toggle_class("#target", "hover"),
852        )
853        .capture()
854        .once()
855        .passive();
856
857        assert_eq!(binding.selector, "#target");
858        assert_eq!(binding.event_type, EventType::MouseEnter);
859        assert!(binding.capture);
860        assert!(binding.once);
861        assert!(binding.passive);
862    }
863
864    // ============================================================
865    // EventBrick tests
866    // ============================================================
867
868    #[test]
869    fn test_event_brick_generation() {
870        let events = EventBrick::new()
871            .on(
872                "#record",
873                EventType::Click,
874                EventHandler::dispatch_state("toggle"),
875            )
876            .on("#clear", EventType::Click, EventHandler::call_wasm("clear"));
877
878        let js = events.to_event_js();
879
880        assert!(js.contains("Generated by probar"));
881        assert!(js.contains("#record"));
882        assert!(js.contains("#clear"));
883    }
884
885    #[test]
886    fn test_event_brick_new() {
887        let brick = EventBrick::new();
888        assert!(brick.selectors().is_empty());
889    }
890
891    #[test]
892    fn test_event_brick_default() {
893        let brick = EventBrick::default();
894        assert!(brick.selectors().is_empty());
895    }
896
897    #[test]
898    fn test_event_brick_on() {
899        let brick = EventBrick::new()
900            .on("#a", EventType::Click, EventHandler::PreventDefault)
901            .on("#b", EventType::Focus, EventHandler::call_wasm("onFocus"));
902
903        let selectors = brick.selectors();
904        assert_eq!(selectors.len(), 2);
905        assert!(selectors.contains(&"#a"));
906        assert!(selectors.contains(&"#b"));
907    }
908
909    #[test]
910    fn test_event_brick_on_with() {
911        let binding = EventBinding::new(
912            "#custom",
913            EventType::TouchEnd,
914            EventHandler::dispatch_state("touch"),
915        )
916        .passive()
917        .once();
918
919        let brick = EventBrick::new().on_with(binding);
920
921        let js = brick.to_event_js();
922        assert!(js.contains("#custom"));
923        assert!(js.contains("touchend"));
924        assert!(js.contains("passive: true"));
925        assert!(js.contains("once: true"));
926    }
927
928    #[test]
929    fn test_event_brick_on_window() {
930        let brick = EventBrick::new()
931            .on_window(EventType::Scroll, EventHandler::call_wasm("onScroll"))
932            .on_window(EventType::KeyDown, EventHandler::dispatch_state("keydown"));
933
934        let js = brick.to_event_js();
935
936        assert!(js.contains("window.addEventListener('scroll'"));
937        assert!(js.contains("window.addEventListener('keydown'"));
938    }
939
940    #[test]
941    fn test_event_brick_selectors() {
942        let brick = EventBrick::new()
943            .on("#one", EventType::Click, EventHandler::PreventDefault)
944            .on(".two", EventType::Input, EventHandler::call_wasm("input"))
945            .on(
946                "[data-id]",
947                EventType::Change,
948                EventHandler::dispatch_state("change"),
949            );
950
951        let selectors = brick.selectors();
952        assert_eq!(selectors.len(), 3);
953        assert!(selectors.contains(&"#one"));
954        assert!(selectors.contains(&".two"));
955        assert!(selectors.contains(&"[data-id]"));
956    }
957
958    #[test]
959    fn test_event_brick_to_event_js_empty() {
960        let brick = EventBrick::new();
961        let js = brick.to_event_js();
962
963        assert!(js.contains("Event Handlers"));
964        assert!(js.contains("Generated by probar"));
965    }
966
967    #[test]
968    fn test_event_brick_debug_and_clone() {
969        let brick = EventBrick::new().on("#test", EventType::Click, EventHandler::PreventDefault);
970
971        let cloned = brick;
972        assert_eq!(cloned.selectors().len(), 1);
973        assert!(format!("{:?}", cloned).contains("EventBrick"));
974    }
975
976    // ============================================================
977    // Brick trait implementation tests
978    // ============================================================
979
980    #[test]
981    fn test_event_brick_brick_name() {
982        let brick = EventBrick::new();
983        assert_eq!(brick.brick_name(), "EventBrick");
984    }
985
986    #[test]
987    fn test_event_brick_assertions() {
988        let brick = EventBrick::new();
989        assert!(brick.assertions().is_empty());
990    }
991
992    #[test]
993    fn test_event_brick_budget() {
994        let brick = EventBrick::new();
995        let budget = brick.budget();
996        assert_eq!(budget.as_duration(), Duration::from_millis(100));
997    }
998
999    #[test]
1000    fn test_event_brick_verify() {
1001        let brick = EventBrick::new();
1002        let verification = brick.verify();
1003
1004        assert!(verification.is_valid());
1005        assert_eq!(verification.passed.len(), 1);
1006        assert!(verification.failed.is_empty());
1007    }
1008
1009    #[test]
1010    fn test_event_brick_to_html() {
1011        let brick = EventBrick::new();
1012        assert!(brick.to_html().is_empty());
1013    }
1014
1015    #[test]
1016    fn test_event_brick_to_css() {
1017        let brick = EventBrick::new();
1018        assert!(brick.to_css().is_empty());
1019    }
1020
1021    // ============================================================
1022    // Integration tests
1023    // ============================================================
1024
1025    #[test]
1026    fn test_complex_event_brick() {
1027        let brick = EventBrick::new()
1028            .on(
1029                "#record-btn",
1030                EventType::Click,
1031                EventHandler::chain(vec![
1032                    EventHandler::PreventDefault,
1033                    EventHandler::when(
1034                        "window.isRecording",
1035                        EventHandler::chain(vec![
1036                            EventHandler::call_wasm("stop_recording"),
1037                            EventHandler::toggle_class("#record-btn", "recording"),
1038                            EventHandler::update_element("#status", "textContent", "'Stopped'"),
1039                        ]),
1040                        Some(EventHandler::chain(vec![
1041                            EventHandler::call_wasm("start_recording"),
1042                            EventHandler::toggle_class("#record-btn", "recording"),
1043                            EventHandler::update_element("#status", "textContent", "'Recording'"),
1044                        ])),
1045                    ),
1046                ]),
1047            )
1048            .on(
1049                "#clear-btn",
1050                EventType::Click,
1051                EventHandler::chain(vec![
1052                    EventHandler::call_wasm("clear_transcript"),
1053                    EventHandler::update_element("#transcript", "textContent", "''"),
1054                ]),
1055            )
1056            .on_window(
1057                EventType::KeyDown,
1058                EventHandler::when(
1059                    "e.key === 'Escape'",
1060                    EventHandler::call_wasm("cancel_recording"),
1061                    None,
1062                ),
1063            );
1064
1065        let js = brick.to_event_js();
1066
1067        // Verify structure
1068        assert!(js.contains("#record-btn"));
1069        assert!(js.contains("#clear-btn"));
1070        assert!(js.contains("window.addEventListener('keydown'"));
1071        assert!(js.contains("window.isRecording"));
1072        assert!(js.contains("stop_recording"));
1073        assert!(js.contains("start_recording"));
1074        assert!(js.contains("e.key === 'Escape'"));
1075    }
1076
1077    #[test]
1078    fn test_event_binding_with_custom_event() {
1079        let binding = EventBinding::new(
1080            "#custom-element",
1081            EventType::Custom("my-custom-event"),
1082            EventHandler::post_to_worker("customWorker", "handle"),
1083        );
1084
1085        let js = binding.to_js();
1086
1087        assert!(js.contains("my-custom-event"));
1088        assert!(js.contains("#custom-element"));
1089        assert!(js.contains("customWorker.postMessage"));
1090    }
1091
1092    #[test]
1093    fn test_nested_conditional_handlers() {
1094        let handler = EventHandler::when(
1095            "conditionA",
1096            EventHandler::when(
1097                "conditionB",
1098                EventHandler::call_wasm("bothTrue"),
1099                Some(EventHandler::call_wasm("onlyATrue")),
1100            ),
1101            Some(EventHandler::call_wasm("aFalse")),
1102        );
1103
1104        let js = handler.to_js(0);
1105
1106        assert!(js.contains("if (conditionA)"));
1107        assert!(js.contains("if (conditionB)"));
1108        assert!(js.contains("bothTrue"));
1109        assert!(js.contains("onlyATrue"));
1110        assert!(js.contains("aFalse"));
1111    }
1112}