1use super::{Brick, BrickAssertion, BrickBudget, BrickVerification};
19use std::time::Duration;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum EventType {
24 Click,
26 DoubleClick,
28 MouseDown,
30 MouseUp,
32 MouseEnter,
34 MouseLeave,
36 KeyDown,
38 KeyUp,
40 KeyPress,
42 Input,
44 Change,
46 Submit,
48 Focus,
50 Blur,
52 Scroll,
54 TouchStart,
56 TouchEnd,
58 TouchMove,
60 Custom(&'static str),
62}
63
64impl EventType {
65 #[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#[derive(Debug, Clone)]
94pub enum EventHandler {
95 DispatchState(String),
97
98 CallWasm {
100 function: String,
102 args: Vec<String>,
104 },
105
106 PostMessage {
108 worker: String,
110 message_type: String,
112 fields: Vec<(String, String)>,
114 },
115
116 UpdateElement {
118 selector: String,
120 property: String,
122 value: String,
124 },
125
126 ToggleClass {
128 selector: String,
130 class: String,
132 },
133
134 PreventDefault,
136
137 Chain(Vec<EventHandler>),
139
140 If {
142 condition: String,
144 then: Box<EventHandler>,
146 otherwise: Option<Box<EventHandler>>,
148 },
149}
150
151impl EventHandler {
152 #[must_use]
154 pub fn dispatch_state(state: impl Into<String>) -> Self {
155 Self::DispatchState(state.into())
156 }
157
158 #[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 #[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 #[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 #[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 #[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 #[must_use]
211 pub fn chain(handlers: Vec<EventHandler>) -> Self {
212 Self::Chain(handlers)
213 }
214
215 #[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 #[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#[derive(Debug, Clone)]
317pub struct EventBinding {
318 pub selector: String,
320 pub event_type: EventType,
322 pub handler: EventHandler,
324 pub capture: bool,
326 pub once: bool,
328 pub passive: bool,
330}
331
332impl EventBinding {
333 #[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 #[must_use]
348 pub fn capture(mut self) -> Self {
349 self.capture = true;
350 self
351 }
352
353 #[must_use]
355 pub fn once(mut self) -> Self {
356 self.once = true;
357 self
358 }
359
360 #[must_use]
362 pub fn passive(mut self) -> Self {
363 self.passive = true;
364 self
365 }
366
367 #[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#[derive(Debug, Clone, Default)]
400pub struct EventBrick {
401 bindings: Vec<EventBinding>,
403 window_handlers: Vec<(EventType, EventHandler)>,
405}
406
407impl EventBrick {
408 #[must_use]
410 pub fn new() -> Self {
411 Self::default()
412 }
413
414 #[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 #[must_use]
429 pub fn on_with(mut self, binding: EventBinding) -> Self {
430 self.bindings.push(binding);
431 self
432 }
433
434 #[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 #[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 for binding in &self.bindings {
451 js.push_str(&binding.to_js());
452 js.push('\n');
453 }
454
455 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 #[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 #[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 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 #[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 #[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 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 #[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 #[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 #[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 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}